Provide expert-level guidance on Prisma ORM, including schema design, relations, migrations, transactions, advanced querying, performance optimization, and production deployment patterns.
Key Patterns
1. Schema Design
// schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["fullTextSearchPostgres", "relationJoins"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Base mixin pattern (use @@map for snake_case table names)
model User {
id String @id @default(cuid())
email String @unique
name String
role Role @default(MEMBER)
isActive Boolean @default(true) @map("is_active")
// Relations
posts Post[]
profile Profile?
sessions Session[]
// Audit fields
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
// Indexes
@@index([email, isActive])
@@index([role, createdAt(sort: Desc)])
@@map("users")
}
enum Role {
ADMIN
MEMBER
VIEWER
}
model Post {
id String @id @default(cuid())
title String
slug String @unique
content String
published Boolean @default(false)
publishedAt DateTime? @map("published_at")
// Relations
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String @map("author_id")
categories CategoriesOnPosts[]
tags String[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Compound index for common queries
@@index([authorId, published, createdAt(sort: Desc)])
@@index([published, publishedAt(sort: Desc)])
@@map("posts")
}
model Category {
id String @id @default(cuid())
name String @unique
slug String @unique
posts CategoriesOnPosts[]
@@map("categories")
}
// Explicit many-to-many for extra fields
model CategoriesOnPosts {
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId String @map("post_id")
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
categoryId String @map("category_id")
assignedAt DateTime @default(now()) @map("assigned_at")
@@id([postId, categoryId])
@@map("categories_on_posts")
}
model Profile {
id String @id @default(cuid())
bio String?
avatar String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String @unique @map("user_id")
@@map("profiles")
}
// Type-safe raw queriesconst result = await prisma.$queryRaw<Array<{ id: string; rank: number }>>`
SELECT id, RANK() OVER (ORDER BY view_count DESC) as rank
FROM posts
WHERE published = true
AND created_at > ${thirtyDaysAgo}
LIMIT ${limit}
`;
// For complex queries not supported by Prisma Clientconst searchResults = await prisma.$queryRaw`
SELECT p.id, p.title, p.slug,
ts_rank(to_tsvector('english', p.title || ' ' || p.content), query) AS rank
FROM posts p, to_tsquery('english', ${searchQuery}) query
WHERE p.published = true
AND to_tsvector('english', p.title || ' ' || p.content) @@ query
ORDER BY rank DESC
LIMIT 20
`;
Best Practices
Use select over include when you don't need all fields -- reduces data transfer
Use cursor-based pagination for production -- offset pagination degrades on large tables
Add @@index for every where + orderBy combination you frequently query
Use interactive transactions ($transaction(async (tx) => {})) for complex business logic
Set maxWait and timeout on transactions to prevent deadlocks
Use Prisma extensions for cross-cutting concerns (soft delete, logging, audit)
Map to snake_case with @@map and @map for database-conventional naming
Use cuid() or uuid() for primary keys, not auto-increment integers
Run prisma migrate deploy in CI/CD, not prisma migrate dev
Monitor query performance with the logging extension pattern
Common Pitfalls
Pitfall
Impact
Fix
N+1 queries from missing include
Excessive DB round trips
Use include or select with nested relations
Using findMany without take
Loading entire tables
Always paginate with take + cursor/offset
Not adding indexes for query patterns
Full table scans
@@index on every filtered/sorted field combination
prisma migrate dev in production
Data loss risk
Use prisma migrate deploy in production
Not handling PrismaClientKnownRequestError
Untyped error responses
Check error.code (P2002 = unique, P2025 = not found)