# Prisma Best Practices ## Schema Design ### Naming Conventions **Models**: PascalCase, singular ```prisma model User { } // [OK] Good model users { } // [ERROR] Bad ``` **Fields**: camelCase ```prisma model User { createdAt DateTime // [OK] Good created_at DateTime // [ERROR] Bad (unless mapping to existing DB) } ``` **Database names**: snake_case with `@@map` and `@map` ```prisma model Profile { avatarUrl String @map("avatar_url") @@map("profiles") } ``` ### ID Fields **Prefer UUIDs for distributed systems:** ```prisma id String @id @default(uuid()) @db.Uuid ``` **Use auto-increment for simple cases:** ```prisma id Int @id @default(autoincrement()) ``` ### Timestamps Always include creation and update timestamps: ```prisma 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 ```prisma model Post { authorId String @db.Uuid published Boolean publishedAt DateTime? @@index([authorId]) @@index([published, publishedAt]) } ``` ### Relations **One-to-many:** ```prisma model User { posts Post[] } model Post { authorId String @db.Uuid author User @relation(fields: [authorId], references: [id]) @@index([authorId]) } ``` **Many-to-many:** ```prisma 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:** ```prisma author User @relation(fields: [authorId], references: [id], onDelete: Cascade) ``` ## Query Optimization ### Select Only Needed Fields ```typescript // [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 ```typescript // [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: ```typescript // 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: ```typescript // [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: ```typescript // 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: ```prisma 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: ```bash 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: ```bash 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: ```sql -- 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 ```typescript 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: ```typescript 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 ```typescript 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 ```typescript // 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 ```typescript // [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 ```typescript // [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 ```typescript // [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 ```prisma model User { deletedAt DateTime? @map("deleted_at") } // Query only non-deleted const activeUsers = await prisma.user.findMany({ where: { deletedAt: null }, }); ``` ## Performance Tips 1. **Use indexes** on filtered/sorted fields 2. **Limit result sets** with take/skip 3. **Select only needed fields** to reduce data transfer 4. **Use connection pooling** for serverless environments 5. **Batch operations** instead of loops 6. **Cache frequent queries** at application level 7. **Monitor slow queries** in Supabase dashboard 8. **Use database views** for complex, repeated queries