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 resetUsage 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#
- Use the singleton pattern - Prevent connection exhaustion in development
- Enable query logging in dev - Debug slow queries
- Add indexes for foreign keys - Improve query performance
- Use transactions for related operations - Ensure data consistency
- Validate input before database calls - Use Zod for type-safe validation
Related Patterns#
- Transactions - Transaction handling
- Migrations - Migration strategies
- Soft Delete - Soft delete implementation