7.7 KiB
Prisma Best Practices
Schema Design
Naming Conventions
Models: PascalCase, singular
model User { } // [OK] Good
model users { } // [ERROR] Bad
Fields: camelCase
model User {
createdAt DateTime // [OK] Good
created_at DateTime // [ERROR] Bad (unless mapping to existing DB)
}
Database names: snake_case with @@map and @map
model Profile {
avatarUrl String @map("avatar_url")
@@map("profiles")
}
ID Fields
Prefer UUIDs for distributed systems:
id String @id @default(uuid()) @db.Uuid
Use auto-increment for simple cases:
id Int @id @default(autoincrement())
Timestamps
Always include creation and update timestamps:
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
Indexes
Add indexes on:
- Foreign keys (automatically indexed)
- Frequently queried fields
- Unique constraints
- Compound queries
model Post {
authorId String @db.Uuid
published Boolean
publishedAt DateTime?
@@index([authorId])
@@index([published, publishedAt])
}
Relations
One-to-many:
model User {
posts Post[]
}
model Post {
authorId String @db.Uuid
author User @relation(fields: [authorId], references: [id])
@@index([authorId])
}
Many-to-many:
model Post {
tags PostTag[]
}
model Tag {
posts PostTag[]
}
model PostTag {
postId String @db.Uuid
post Post @relation(fields: [postId], references: [id])
tagId String @db.Uuid
tag Tag @relation(fields: [tagId], references: [id])
@@id([postId, tagId])
}
Cascade deletes:
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
Query Optimization
Select Only Needed Fields
// [ERROR] Bad - fetches all fields
const users = await prisma.user.findMany();
// [OK] Good - only fetch what you need
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
},
});
Use Include Wisely
// [ERROR] Bad - deep nesting can be slow
const posts = await prisma.post.findMany({
include: {
author: {
include: {
profile: {
include: {
settings: true,
},
},
},
},
},
});
// [OK] Good - only include what you display
const posts = await prisma.post.findMany({
include: {
author: {
select: {
id: true,
name: true,
},
},
},
});
Pagination
Always paginate large result sets:
// Offset pagination
const posts = await prisma.post.findMany({
skip: 20,
take: 10,
});
// Cursor-based pagination (better for large datasets)
const posts = await prisma.post.findMany({
take: 10,
cursor: {
id: lastPostId,
},
});
Batch Operations
Use batch operations for bulk inserts/updates:
// [OK] Single query instead of N queries
await prisma.user.createMany({
data: [
{ email: 'user1@example.com', name: 'User 1' },
{ email: 'user2@example.com', name: 'User 2' },
],
skipDuplicates: true,
});
Connection Management
Singleton Pattern (Next.js)
Prevent connection exhaustion in development:
// 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;
}
Connection Pooling
Use Supabase connection pooler for better performance:
datasource db {
provider = "postgresql"
url = env("DIRECT_URL") // Pooled connection
directUrl = env("DATABASE_URL") // Direct for migrations
}
Migration Best Practices
1. Review Generated SQL
Always check migration.sql before applying:
npx prisma migrate dev --name add_users --create-only
# Review prisma/migrations/.../migration.sql
npx prisma migrate dev
2. Atomic Migrations
One logical change per migration:
- [OK] Good: "add_user_roles"
- [ERROR] Bad: "add_users_and_posts_and_comments"
3. Production Migrations
Use migrate deploy in production:
npx prisma migrate deploy
Never use migrate dev or migrate reset in production.
4. Handle Data Migrations
For complex data transformations, use multi-step migrations:
-- Step 1: Add new column with default
ALTER TABLE "User" ADD COLUMN "full_name" TEXT;
-- Step 2: Populate from existing data
UPDATE "User" SET "full_name" = "first_name" || ' ' || "last_name";
-- Step 3: Make non-nullable
ALTER TABLE "User" ALTER COLUMN "full_name" SET NOT NULL;
5. Rollback Strategy
Migrations can't be rolled back automatically. Plan for:
- Backup before major migrations
- Keep old columns temporarily
- Deploy in stages
Error Handling
Handle Unique Constraint Violations
try {
await prisma.user.create({
data: { email: 'test@example.com' },
});
} catch (error) {
if (error.code === 'P2002') {
// Unique constraint violation
throw new Error('Email already exists');
}
throw error;
}
Use Transactions
For operations that must succeed or fail together:
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email: 'test@example.com' },
});
await tx.profile.create({
data: {
userId: user.id,
bio: 'New user',
},
});
});
TypeScript Integration
Leverage Generated Types
import { Prisma } from '@prisma/client';
// Use generated types for input
type UserCreateInput = Prisma.UserCreateInput;
// Type-safe queries
const where: Prisma.UserWhereInput = {
email: {
contains: '@example.com',
},
};
Type Query Results
// Get inferred type from query
const user = await prisma.user.findUnique({
where: { id: '1' },
include: { posts: true },
});
type UserWithPosts = typeof user;
Common Pitfalls
1. N+1 Query Problem
// [ERROR] Bad - N+1 queries
const posts = await prisma.post.findMany();
for (const post of posts) {
const author = await prisma.user.findUnique({
where: { id: post.authorId },
});
}
// [OK] Good - single query with include
const posts = await prisma.post.findMany({
include: { author: true },
});
2. Not Using Transactions
// [ERROR] Bad - can leave inconsistent state
await prisma.user.create({ data: userData });
await prisma.profile.create({ data: profileData }); // If this fails, user exists without profile
// [OK] Good - atomic operation
await prisma.$transaction([
prisma.user.create({ data: userData }),
prisma.profile.create({ data: profileData }),
]);
3. Exposing Prisma Client to Frontend
// [ERROR] Bad - never import Prisma in client components
'use client';
import { prisma } from '@/lib/prisma'; // Security risk!
// [OK] Good - use Server Actions or API routes
'use server';
import { prisma } from '@/lib/prisma';
export async function getUsers() {
return await prisma.user.findMany();
}
4. Ignoring Soft Deletes
model User {
deletedAt DateTime? @map("deleted_at")
}
// Query only non-deleted
const activeUsers = await prisma.user.findMany({
where: { deletedAt: null },
});
Performance Tips
- Use indexes on filtered/sorted fields
- Limit result sets with take/skip
- Select only needed fields to reduce data transfer
- Use connection pooling for serverless environments
- Batch operations instead of loops
- Cache frequent queries at application level
- Monitor slow queries in Supabase dashboard
- Use database views for complex, repeated queries