Prisma is the most popular ORM for Next.js. It gives you a type-safe query builder, schema migrations, and a great DX.
1npm i @prisma/client
2npm i -D prisma
3npx prisma initChoose Postgres in the prompts (most flexible).
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 passwordHash String
16 role Role @default(STUDENT)
17 createdAt DateTime @default(now())
18
19 courses Course[]
20 enrollments Enrollment[]
21}
22
23enum Role { STUDENT INSTRUCTOR ADMIN }
24
25model Course {
26 id String @id @default(cuid())
27 slug String @unique
28 title String
29 description String @db.Text
30 createdById String
31
32 createdBy User @relation(fields: [createdById], references: [id])
33 lessons Lesson[]
34 enrollments Enrollment[]
35}
36
37model Lesson {
38 id String @id @default(cuid())
39 courseId String
40 title String
41 content String @db.Text
42 order Int
43
44 course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
45}
46
47model Enrollment {
48 id String @id @default(cuid())
49 userId String
50 courseId String
51 createdAt DateTime @default(now())
52
53 user User @relation(fields: [userId], references: [id])
54 course Course @relation(fields: [courseId], references: [id])
55
56 @@unique([userId, courseId])
57}In dev, hot-reload would create a new PrismaClient on every save → DB connection exhaustion.
1// lib/db.ts
2import "server-only";
3import { PrismaClient } from "@prisma/client";
4
5const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
6
7export const db =
8 globalForPrisma.prisma ??
9 new PrismaClient({ log: process.env.NODE_ENV === "development" ? ["query", "error"] : ["error"] });
10
11if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;After every schema change:
1npx prisma generate # regenerates the typed client
2npx prisma migrate dev --name <reason> # applies a migration in devAdd to package.json so it runs on install:
1{
2 "scripts": {
3 "postinstall": "prisma generate",
4 "db:push": "prisma db push",
5 "db:studio": "prisma studio"
6 }
7}| Command | Use For |
|---|---|
prisma migrate dev | Versioned migrations, committed to git |
prisma db push | Quick prototyping, no migration files |
Use migrate for any project that will reach production. Use db push for fast experimentation only.
1// Find with relations
2const course = await db.course.findUnique({
3 where: { slug },
4 include: { lessons: { orderBy: { order: "asc" } } },
5});
6
7// Aggregate
8const stats = await db.enrollment.groupBy({
9 by: ["courseId"],
10 _count: { _all: true },
11 having: { courseId: { _count: { _all: { gt: 50 } } } },
12});
13
14// Transactions
15await db.$transaction(async (tx) => {
16 await tx.course.update({ where: { id }, data: { status: "PUBLISHED" } });
17 await tx.notification.create({ data: { /* … */ } });
18});In serverless (Vercel), each invocation creates a fresh process. You must front Postgres with a pooler:
-pooler URL.For Prisma + serverless, also enable directUrl for migrations:
1datasource db {
2 url = env("DATABASE_URL") // pooled, used at runtime
3 directUrl = env("DIRECT_URL") // direct, used only for migrations
4}