Tutorial Prisma ORM: Setup dan Best Practices untuk TypeScript
Sabtu, 27 Des 2025
Kalau kamu pernah pakai raw SQL queries atau ORM tradisional seperti Sequelize, pasti tahu rasanya debugging query yang error di runtime. Typo nama kolom? Error baru ketahuan pas aplikasi jalan.
Prisma hadir dengan pendekatan berbeda: type-safe database access dengan auto-generated types dari schema kamu. Artinya, kalau ada typo atau salah query, TypeScript langsung kasih warning sebelum kode dijalankan.
Apa itu ORM?
ORM (Object-Relational Mapping) adalah layer abstraksi antara aplikasi dan database. Daripada nulis raw SQL:
SELECT * FROM users WHERE id = 1;
Kamu bisa pakai syntax yang lebih readable:
const user = await prisma.user.findUnique({ where: { id: 1 } });
Kenapa Prisma?
Prisma bukan ORM biasa. Ini yang bikin Prisma stand out:
| Fitur | Prisma | ORM Tradisional |
|---|---|---|
| Type Safety | 100% type-safe | Partial atau manual |
| Schema Definition | Prisma Schema Language | Decorators/JS Objects |
| Migrations | Automated & versioned | Manual atau semi-auto |
| Query Building | Fluent API dengan autocomplete | String-based atau builder |
| Tooling | Prisma Studio, CLI, VS Code extension | Varies |
Instalasi dan Setup
1. Init Project
mkdir prisma-tutorial && cd prisma-tutorial
npm init -y
npm install typescript ts-node @types/node -D
npx tsc --init
2. Install Prisma
npm install prisma -D
npm install @prisma/client
3. Initialize Prisma
npx prisma init
Ini akan membuat:
prisma/schema.prisma- File schema utama.env- File environment variables
4. Konfigurasi Database
Edit .env dengan connection string database kamu:
# PostgreSQL
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
# MySQL
DATABASE_URL="mysql://user:password@localhost:3306/mydb"
# SQLite (cocok untuk development)
DATABASE_URL="file:./dev.db"
Prisma Schema: Jantung dari Prisma
File prisma/schema.prisma adalah tempat kamu mendefinisikan struktur database:
// filepath: prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
profile Profile?
comments Comment[]
}
enum Role {
USER
ADMIN
MODERATOR
}
Penjelasan Attributes
| Attribute | Fungsi |
|---|---|
@id | Primary key |
@unique | Nilai harus unik |
@default() | Nilai default |
autoincrement() | Auto increment untuk integer |
now() | Timestamp saat record dibuat |
@updatedAt | Auto update timestamp saat record diupdate |
? | Field optional (nullable) |
Relations: 1:1, 1:N, dan N:M
One-to-One (1:1)
User memiliki satu Profile:
model User {
id Int @id @default(autoincrement())
email String @unique
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String?
avatar String?
userId Int @unique
user User @relation(fields: [userId], references: [id])
}
One-to-Many (1:N)
User memiliki banyak Post:
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
}
Many-to-Many (N:M)
Post memiliki banyak Category, Category dimiliki banyak Post:
model Post {
id Int @id @default(autoincrement())
title String
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}
Prisma otomatis membuat junction table. Kalau butuh explicit junction table dengan field tambahan:
model Post {
id Int @id @default(autoincrement())
title String
categories PostCategory[]
}
model Category {
id Int @id @default(autoincrement())
name String @unique
posts PostCategory[]
}
model PostCategory {
postId Int
categoryId Int
assignedAt DateTime @default(now())
post Post @relation(fields: [postId], references: [id])
category Category @relation(fields: [categoryId], references: [id])
@@id([postId, categoryId])
}
Complete Schema Example
Berikut schema lengkap untuk blog platform:
// filepath: prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
profile Profile?
comments Comment[]
}
model Profile {
id Int @id @default(autoincrement())
bio String?
avatar String?
website String?
twitter String?
github String?
userId Int @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Post {
id Int @id @default(autoincrement())
slug String @unique
title String
content String?
excerpt String?
published Boolean @default(false)
publishedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authorId Int
author User @relation(fields: [authorId], references: [id])
categories Category[]
comments Comment[]
tags Tag[]
@@index([authorId])
@@index([published])
}
model Category {
id Int @id @default(autoincrement())
name String @unique
slug String @unique
description String?
posts Post[]
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
slug String @unique
posts Post[]
}
model Comment {
id Int @id @default(autoincrement())
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
postId Int
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
authorId Int
author User @relation(fields: [authorId], references: [id])
parentId Int?
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id])
replies Comment[] @relation("CommentReplies")
@@index([postId])
@@index([authorId])
}
enum Role {
USER
ADMIN
MODERATOR
}
Migrations
Setelah schema siap, jalankan migration:
# Development: Create dan apply migration
npx prisma migrate dev --name init
# Production: Apply pending migrations
npx prisma migrate deploy
# Reset database (hapus semua data!)
npx prisma migrate reset
Tips Migration
- Selalu review SQL yang di-generate - Prisma generate SQL di folder
prisma/migrations/ - Jangan edit migration files - Kalau perlu ubah, buat migration baru
- Gunakan nama yang deskriptif -
add_user_avatarlebih baik dariupdate_1
Generate Prisma Client
Setiap kali schema berubah, regenerate client:
npx prisma generate
Ini akan generate types berdasarkan schema kamu.
CRUD Operations
Setup Prisma Client
// filepath: src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
Pattern ini mencegah multiple PrismaClient instances saat hot reload di development.
Create
// Create single record
const user = await prisma.user.create({
data: {
email: '[email protected]',
name: 'John Doe',
password: 'hashedpassword',
},
});
// Create dengan nested relation
const userWithProfile = await prisma.user.create({
data: {
email: '[email protected]',
name: 'Jane Doe',
password: 'hashedpassword',
profile: {
create: {
bio: 'Full-stack developer',
github: 'janedoe',
},
},
},
include: {
profile: true,
},
});
// Create many
const users = await prisma.user.createMany({
data: [
{ email: '[email protected]', name: 'User 1', password: 'hash1' },
{ email: '[email protected]', name: 'User 2', password: 'hash2' },
],
skipDuplicates: true, // Skip jika email sudah ada
});
Read
// Find by ID
const user = await prisma.user.findUnique({
where: { id: 1 },
});
// Find by unique field
const userByEmail = await prisma.user.findUnique({
where: { email: '[email protected]' },
});
// Find first match
const admin = await prisma.user.findFirst({
where: { role: 'ADMIN' },
});
// Find many
const allUsers = await prisma.user.findMany();
// Find with conditions
const activeAuthors = await prisma.user.findMany({
where: {
role: 'USER',
posts: {
some: {
published: true,
},
},
},
});
Update
// Update single
const updatedUser = await prisma.user.update({
where: { id: 1 },
data: { name: 'John Updated' },
});
// Update many
const deactivated = await prisma.user.updateMany({
where: {
lastLogin: {
lt: new Date('2024-01-01'),
},
},
data: {
role: 'INACTIVE',
},
});
// Upsert (update or create)
const user = await prisma.user.upsert({
where: { email: '[email protected]' },
update: { name: 'John Doe Updated' },
create: {
email: '[email protected]',
name: 'John Doe',
password: 'hashedpassword',
},
});
Delete
// Delete single
const deleted = await prisma.user.delete({
where: { id: 1 },
});
// Delete many
const deletedCount = await prisma.user.deleteMany({
where: {
role: 'INACTIVE',
},
});
Advanced Queries
Select: Pilih Field Spesifik
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
name: true,
// password tidak di-select untuk keamanan
},
});
Include: Load Relations
// Include single relation
const userWithPosts = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: true,
},
});
// Include nested relations
const userComplete = await prisma.user.findUnique({
where: { id: 1 },
include: {
profile: true,
posts: {
include: {
categories: true,
comments: {
include: {
author: {
select: {
id: true,
name: true,
},
},
},
},
},
},
},
});
Where: Filtering
// Equals
const user = await prisma.user.findFirst({
where: { email: '[email protected]' },
});
// Not equals
const nonAdmins = await prisma.user.findMany({
where: { role: { not: 'ADMIN' } },
});
// Contains (case-insensitive by default di PostgreSQL)
const searchResults = await prisma.user.findMany({
where: {
name: { contains: 'john' },
},
});
// Starts with / Ends with
const gmailUsers = await prisma.user.findMany({
where: {
email: { endsWith: '@gmail.com' },
},
});
// In array
const specificRoles = await prisma.user.findMany({
where: {
role: { in: ['ADMIN', 'MODERATOR'] },
},
});
// Greater than, Less than
const recentPosts = await prisma.post.findMany({
where: {
createdAt: { gte: new Date('2024-01-01') },
},
});
// AND, OR, NOT
const complexQuery = await prisma.user.findMany({
where: {
AND: [
{ role: 'USER' },
{
OR: [
{ email: { contains: 'company.com' } },
{ name: { startsWith: 'Admin' } },
],
},
],
NOT: {
posts: { none: {} }, // User yang punya minimal 1 post
},
},
});
Filtering Relations
// Users yang punya minimal 1 post published
const authors = await prisma.user.findMany({
where: {
posts: {
some: {
published: true,
},
},
},
});
// Users yang semua post-nya published
const allPublished = await prisma.user.findMany({
where: {
posts: {
every: {
published: true,
},
},
},
});
// Users tanpa post sama sekali
const noPostUsers = await prisma.user.findMany({
where: {
posts: {
none: {},
},
},
});
Pagination
// Offset-based pagination
const page = 2;
const pageSize = 10;
const posts = await prisma.post.findMany({
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: { createdAt: 'desc' },
});
// Cursor-based pagination (lebih efisien untuk dataset besar)
const nextPosts = await prisma.post.findMany({
take: 10,
skip: 1, // Skip the cursor
cursor: { id: lastPostId },
orderBy: { id: 'asc' },
});
Ordering
// Single order
const posts = await prisma.post.findMany({
orderBy: { createdAt: 'desc' },
});
// Multiple order
const users = await prisma.user.findMany({
orderBy: [
{ role: 'asc' },
{ name: 'asc' },
],
});
// Order by relation
const usersByPostCount = await prisma.user.findMany({
orderBy: {
posts: {
_count: 'desc',
},
},
});
Aggregation
// Count
const userCount = await prisma.user.count();
const adminCount = await prisma.user.count({
where: { role: 'ADMIN' },
});
// Aggregate
const stats = await prisma.post.aggregate({
_count: true,
_avg: { viewCount: true },
_max: { viewCount: true },
_min: { viewCount: true },
where: { published: true },
});
// Group by
const postsByAuthor = await prisma.post.groupBy({
by: ['authorId'],
_count: { id: true },
_avg: { viewCount: true },
having: {
id: {
_count: { gt: 5 },
},
},
orderBy: {
_count: {
id: 'desc',
},
},
});
Transactions
Sequential Transactions
const result = await prisma.$transaction(async (tx) => {
// 1. Create user
const user = await tx.user.create({
data: {
email: '[email protected]',
name: 'New User',
password: 'hashedpassword',
},
});
// 2. Create profile
const profile = await tx.profile.create({
data: {
userId: user.id,
bio: 'Hello world',
},
});
// 3. Create welcome post
const post = await tx.post.create({
data: {
slug: 'welcome-new-user',
title: 'Welcome!',
content: 'Welcome to our platform',
authorId: user.id,
published: true,
},
});
return { user, profile, post };
});
Batch Transactions
// Multiple independent operations
const [users, posts, comments] = await prisma.$transaction([
prisma.user.findMany(),
prisma.post.findMany({ where: { published: true } }),
prisma.comment.count(),
]);
Interactive Transactions dengan Timeout
const result = await prisma.$transaction(
async (tx) => {
// Operasi yang mungkin lama
const users = await tx.user.findMany();
for (const user of users) {
await tx.user.update({
where: { id: user.id },
data: { lastProcessed: new Date() },
});
}
return users.length;
},
{
maxWait: 5000, // 5 detik max waiting untuk acquire connection
timeout: 30000, // 30 detik timeout untuk transaction
}
);
Database Seeding
Buat file seed untuk populate data awal:
// filepath: prisma/seed.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Clean existing data
await prisma.comment.deleteMany();
await prisma.post.deleteMany();
await prisma.profile.deleteMany();
await prisma.category.deleteMany();
await prisma.tag.deleteMany();
await prisma.user.deleteMany();
console.log('Seeding database...');
// Create users
const admin = await prisma.user.create({
data: {
email: '[email protected]',
name: 'Admin User',
password: '$2b$10$hashedpassword', // Use bcrypt in real app
role: 'ADMIN',
profile: {
create: {
bio: 'Platform administrator',
github: 'admin',
},
},
},
});
const author = await prisma.user.create({
data: {
email: '[email protected]',
name: 'John Author',
password: '$2b$10$hashedpassword',
role: 'USER',
profile: {
create: {
bio: 'Tech writer and developer',
website: 'https://johnauthor.dev',
twitter: 'johnauthor',
github: 'johnauthor',
},
},
},
});
// Create categories
const categories = await Promise.all([
prisma.category.create({
data: { name: 'Technology', slug: 'technology', description: 'Tech articles' },
}),
prisma.category.create({
data: { name: 'Tutorial', slug: 'tutorial', description: 'How-to guides' },
}),
prisma.category.create({
data: { name: 'Opinion', slug: 'opinion', description: 'Opinion pieces' },
}),
]);
// Create tags
const tags = await Promise.all([
prisma.tag.create({ data: { name: 'JavaScript', slug: 'javascript' } }),
prisma.tag.create({ data: { name: 'TypeScript', slug: 'typescript' } }),
prisma.tag.create({ data: { name: 'Prisma', slug: 'prisma' } }),
prisma.tag.create({ data: { name: 'Database', slug: 'database' } }),
]);
// Create posts
const post1 = await prisma.post.create({
data: {
slug: 'getting-started-with-prisma',
title: 'Getting Started with Prisma',
content: 'Prisma is a next-generation ORM...',
excerpt: 'Learn how to use Prisma in your projects',
published: true,
publishedAt: new Date(),
authorId: author.id,
categories: {
connect: [{ id: categories[0].id }, { id: categories[1].id }],
},
tags: {
connect: [{ id: tags[2].id }, { id: tags[3].id }],
},
},
});
const post2 = await prisma.post.create({
data: {
slug: 'typescript-best-practices',
title: 'TypeScript Best Practices in 2025',
content: 'TypeScript has evolved significantly...',
excerpt: 'Master TypeScript with these best practices',
published: true,
publishedAt: new Date(),
authorId: author.id,
categories: {
connect: [{ id: categories[0].id }],
},
tags: {
connect: [{ id: tags[0].id }, { id: tags[1].id }],
},
},
});
// Create comments
await prisma.comment.create({
data: {
content: 'Great article! Very helpful.',
postId: post1.id,
authorId: admin.id,
},
});
console.log('Seeding completed!');
console.log({
users: await prisma.user.count(),
posts: await prisma.post.count(),
categories: await prisma.category.count(),
tags: await prisma.tag.count(),
comments: await prisma.comment.count(),
});
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
Tambahkan di package.json:
{
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}
Jalankan seeding:
npx prisma db seed
Prisma Studio
Prisma Studio adalah GUI untuk explore dan edit data:
npx prisma studio
Akan membuka browser di http://localhost:5555. Kamu bisa:
- Browse semua tables
- View dan edit records
- Filter dan sort data
- Lihat relations
Performance Tips
1. Gunakan Select untuk Limit Fields
// ❌ Mengambil semua fields termasuk password
const users = await prisma.user.findMany();
// ✅ Hanya ambil yang diperlukan
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
name: true,
},
});
2. Batasi Nested Includes
// ❌ Deep nesting = banyak JOIN = slow
const post = await prisma.post.findUnique({
where: { id: 1 },
include: {
author: {
include: {
posts: {
include: {
comments: {
include: {
author: true,
},
},
},
},
},
},
},
});
// ✅ Pisahkan ke multiple queries jika perlu
const post = await prisma.post.findUnique({
where: { id: 1 },
include: { author: true },
});
const comments = await prisma.comment.findMany({
where: { postId: 1 },
include: { author: { select: { id: true, name: true } } },
take: 20,
});
3. Gunakan Index di Schema
model Post {
id Int @id @default(autoincrement())
slug String @unique
authorId Int
published Boolean @default(false)
createdAt DateTime @default(now())
@@index([authorId])
@@index([published])
@@index([createdAt])
@@index([published, createdAt]) // Composite index
}
4. Connection Pooling
Untuk serverless atau high-traffic:
// filepath: src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
});
export default prisma;
Untuk Vercel/serverless, gunakan Prisma Accelerate atau connection pooler seperti PgBouncer.
5. Batch Operations
// ❌ Multiple round trips
for (const id of userIds) {
await prisma.user.update({
where: { id },
data: { lastSeen: new Date() },
});
}
// ✅ Single query
await prisma.user.updateMany({
where: { id: { in: userIds } },
data: { lastSeen: new Date() },
});
Best Practices
1. Struktur Project
src/
├── lib/
│ └── prisma.ts # Singleton Prisma client
├── repositories/ # Data access layer
│ ├── user.repository.ts
│ └── post.repository.ts
├── services/ # Business logic
│ ├── user.service.ts
│ └── post.service.ts
└── ...
prisma/
├── schema.prisma
├── seed.ts
└── migrations/
2. Repository Pattern
// filepath: src/repositories/user.repository.ts
import prisma from '@/lib/prisma';
import { Prisma } from '@prisma/client';
export const userRepository = {
async findById(id: number) {
return prisma.user.findUnique({
where: { id },
select: {
id: true,
email: true,
name: true,
role: true,
profile: true,
},
});
},
async findByEmail(email: string) {
return prisma.user.findUnique({
where: { email },
});
},
async create(data: Prisma.UserCreateInput) {
return prisma.user.create({ data });
},
async update(id: number, data: Prisma.UserUpdateInput) {
return prisma.user.update({
where: { id },
data,
});
},
async delete(id: number) {
return prisma.user.delete({
where: { id },
});
},
async findManyWithPagination({
page = 1,
pageSize = 10,
where,
orderBy,
}: {
page?: number;
pageSize?: number;
where?: Prisma.UserWhereInput;
orderBy?: Prisma.UserOrderByWithRelationInput;
}) {
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
orderBy: orderBy ?? { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
select: {
id: true,
email: true,
name: true,
role: true,
createdAt: true,
},
}),
prisma.user.count({ where }),
]);
return {
users,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize),
},
};
},
};
3. Error Handling
import { Prisma } from '@prisma/client';
async function createUser(data: Prisma.UserCreateInput) {
try {
return await prisma.user.create({ data });
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
// P2002: Unique constraint violation
if (error.code === 'P2002') {
throw new Error('Email already exists');
}
// P2025: Record not found
if (error.code === 'P2025') {
throw new Error('User not found');
}
}
throw error;
}
}
4. Type-Safe Includes dengan Satisfies
import { Prisma } from '@prisma/client';
const postWithAuthor = {
include: {
author: {
select: {
id: true,
name: true,
email: true,
},
},
categories: true,
},
} satisfies Prisma.PostDefaultArgs;
type PostWithAuthor = Prisma.PostGetPayload<typeof postWithAuthor>;
async function getPost(id: number): Promise<PostWithAuthor | null> {
return prisma.post.findUnique({
where: { id },
...postWithAuthor,
});
}
5. Environment-Specific Configuration
// filepath: src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const prismaClientSingleton = () => {
return new PrismaClient({
log:
process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
errorFormat: process.env.NODE_ENV === 'development' ? 'pretty' : 'minimal',
});
};
declare global {
var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
}
const prisma = globalThis.prisma ?? prismaClientSingleton();
if (process.env.NODE_ENV !== 'production') {
globalThis.prisma = prisma;
}
export default prisma;
Kesimpulan
Prisma adalah game-changer untuk database access di TypeScript:
- Type safety - Error terdeteksi saat compile time
- Developer experience - Autocomplete, Prisma Studio, excellent docs
- Migrations - Versioned, automated, predictable
- Performance - Query optimization, connection pooling support
- Ecosystem - Prisma Accelerate, Pulse, dan growing community
Mulai dengan schema yang simple, pahami relations dan queries, lalu scale up dengan best practices yang sudah dibahas.