Prisma ORM

Battle-tested patterns for Prisma ORM with Next.js applications.

Overview#

Prisma provides type-safe database access:

  • Auto-generated TypeScript types
  • Intuitive query API
  • Database migrations
  • Visual database browser (Prisma Studio)

Prerequisites:

  • Node.js 18+
  • PostgreSQL, MySQL, or SQLite database

Installation#

npm install prisma @prisma/client npx prisma init
# .env DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"

Implementation#

Client Singleton (Prevents Connection Exhaustion)#

1// lib/db.ts 2import { PrismaClient } from '@prisma/client' 3 4const globalForPrisma = globalThis as unknown as { 5 prisma: PrismaClient | undefined 6} 7 8export const prisma = globalForPrisma.prisma ?? new PrismaClient({ 9 log: process.env.NODE_ENV === 'development' 10 ? ['query', 'error', 'warn'] 11 : ['error'], 12}) 13 14if (process.env.NODE_ENV !== 'production') { 15 globalForPrisma.prisma = prisma 16}

Common Query Patterns#

1// Create with relations 2const user = await prisma.user.create({ 3 data: { 4 email: 'john@example.com', 5 name: 'John Doe', 6 profile: { 7 create: { bio: 'Hello world' } 8 } 9 }, 10 include: { profile: true } 11}) 12 13// Read with filters and pagination 14const posts = await prisma.post.findMany({ 15 where: { 16 published: true, 17 author: { email: { contains: '@example.com' } } 18 }, 19 include: { author: { select: { name: true } } }, 20 orderBy: { createdAt: 'desc' }, 21 skip: (page - 1) * pageSize, 22 take: pageSize 23}) 24 25// Update 26const updated = await prisma.user.update({ 27 where: { id: userId }, 28 data: { name: 'Updated Name' } 29}) 30 31// Upsert (create or update) 32const result = await prisma.user.upsert({ 33 where: { email: 'user@example.com' }, 34 update: { name: 'Updated' }, 35 create: { email: 'user@example.com', name: 'New User' } 36}) 37 38// Soft delete pattern 39const deleted = await prisma.user.update({ 40 where: { id: userId }, 41 data: { deletedAt: new Date() } 42})

Transactions#

1// Sequential transaction (atomic) 2const [user, post] = await prisma.$transaction([ 3 prisma.user.create({ data: userData }), 4 prisma.post.create({ data: postData }) 5]) 6 7// Interactive transaction (with logic) 8await prisma.$transaction(async (tx) => { 9 const user = await tx.user.findUnique({ where: { id: userId } }) 10 if (!user) throw new Error('User not found') 11 if (user.credits < 10) throw new Error('Insufficient credits') 12 13 await tx.user.update({ 14 where: { id: userId }, 15 data: { credits: { decrement: 10 } } 16 }) 17 18 await tx.transaction.create({ 19 data: { userId, amount: -10, type: 'PURCHASE' } 20 }) 21})

Server Action Pattern#

1// actions/user.ts 2'use server' 3import { prisma } from '@/lib/db' 4import { revalidatePath } from 'next/cache' 5import { z } from 'zod' 6 7const UpdateProfileSchema = z.object({ 8 name: z.string().min(1).max(100), 9 bio: z.string().max(500).optional() 10}) 11 12export async function updateProfile(userId: string, formData: FormData) { 13 const validated = UpdateProfileSchema.parse({ 14 name: formData.get('name'), 15 bio: formData.get('bio') 16 }) 17 18 await prisma.user.update({ 19 where: { id: userId }, 20 data: validated 21 }) 22 23 revalidatePath('/profile') 24 return { success: true } 25}

Schema Example#

1// prisma/schema.prisma 2generator client { 3 provider = "prisma-client-js" 4} 5 6datasource db { 7 provider = "postgresql" 8 url = env("DATABASE_URL") 9} 10 11model User { 12 id String @id @default(cuid()) 13 email String @unique 14 name String? 15 credits Int @default(0) 16 createdAt DateTime @default(now()) 17 updatedAt DateTime @updatedAt 18 deletedAt DateTime? 19 20 posts Post[] 21 profile Profile? 22 23 @@index([email]) 24 @@map("users") 25} 26 27model Post { 28 id String @id @default(cuid()) 29 title String 30 content String? 31 published Boolean @default(false) 32 authorId String 33 author User @relation(fields: [authorId], references: [id]) 34 createdAt DateTime @default(now()) 35 36 @@index([authorId]) 37 @@map("posts") 38} 39 40model Profile { 41 id String @id @default(cuid()) 42 bio String? 43 avatar String? 44 userId String @unique 45 user User @relation(fields: [userId], references: [id], onDelete: Cascade) 46}

Migration Commands#

1# Development: create and apply migration 2npx prisma migrate dev --name add_posts_table 3 4# Production: apply pending migrations 5npx prisma migrate deploy 6 7# Regenerate client after schema changes 8npx prisma generate 9 10# Visual database browser 11npx prisma studio 12 13# Reset database (WARNING: destroys data) 14npx prisma migrate reset

Usage Examples#

API Route#

1// app/api/users/[id]/route.ts 2import { NextResponse } from 'next/server' 3import { prisma } from '@/lib/db' 4 5export async function GET( 6 request: Request, 7 { params }: { params: { id: string } } 8) { 9 const user = await prisma.user.findUnique({ 10 where: { id: params.id }, 11 include: { profile: true } 12 }) 13 14 if (!user) { 15 return NextResponse.json({ error: 'Not found' }, { status: 404 }) 16 } 17 18 return NextResponse.json(user) 19}

Server Component#

1// app/users/page.tsx 2import { prisma } from '@/lib/db' 3 4export default async function UsersPage() { 5 const users = await prisma.user.findMany({ 6 where: { deletedAt: null }, 7 orderBy: { createdAt: 'desc' }, 8 take: 10 9 }) 10 11 return ( 12 <ul> 13 {users.map(user => ( 14 <li key={user.id}>{user.name}</li> 15 ))} 16 </ul> 17 ) 18}

Best Practices#

  1. Use the singleton pattern - Prevent connection exhaustion in development
  2. Enable query logging in dev - Debug slow queries
  3. Add indexes for foreign keys - Improve query performance
  4. Use transactions for related operations - Ensure data consistency
  5. Validate input before database calls - Use Zod for type-safe validation