What Prisma is and why it fits Next.js
Prisma is an ORM that turns your database tables into a type-safe API (Prisma Client). With Next.js, Prisma helps you avoid handwritten SQL, keeps queries consistent, and provides good TypeScript support. In an App Router project, the main rule is simple:
- Use Prisma only on the server.
- Do not import Prisma Client into browser code.
Prerequisites
- Node.js and pnpm (or npm)
- A database you can connect to (PostgreSQL, MySQL, SQLite)
- A Next.js app using the App Router
Step 1: Install Prisma
From the project root:
pnpm add -D prisma
pnpm add @prisma/clientIf you use npm, replace <code>pnpm</code> with <code>npm</code>.
Step 2: Initialize Prisma
pnpm prisma init --datasource-provider postgresqlThis creates:
- <code>prisma/schema.prisma</code>
- <code>.env</code> with a <code>DATABASE_URL</code>
If your database is not PostgreSQL, use the correct provider.
Step 3: Configure DATABASE_URL
In <code>.env</code>:
DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/DB_NAME?schema=public"Never commit real credentials. Keep <code>.env</code> out of version control.
Step 4: Define your Prisma schema
Edit <code>prisma/schema.prisma</code>. Example schema:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Notes:
- Use <code>cuid()</code> for short, collision resistant IDs.
- Use <code>@unique</code> for fields like <code>email</code>.
Step 5: Create and apply migrations
pnpm prisma migrate dev --name initThis:
- generates a migration
- applies it locally
- updates Prisma Client
For production, you will run migrations differently (for example in CI or during deploy).
Step 6: Add a safe Prisma Client helper for Next.js
The App Router can rerender on every request in development. To avoid creating many Prisma Client instances, create a shared helper. Create <code>lib/prisma.ts</code>:
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma?: PrismaClient;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: ["error", "warn"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;Guidelines:
- This file is server-only by convention.
- Only import <code>prisma</code> from server code.
Step 7: Use Prisma in an API route (App Router)
Create an API route like <code>app/api/users/route.ts</code>. Example CRUD: list users.
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function GET() {
const users = await prisma.user.findMany({
orderBy: { createdAt: "desc" },
select: {
id: true,
email: true,
name: true,
createdAt: true,
},
});
return NextResponse.json({ users });
}Because it runs in <code>app/api/.../route.ts</code>, it is server code.
Step 8: Use Prisma in server components
Server Components can directly call Prisma (as long as you do not import Prisma Client into Client Components). Example <code>app/users/page.tsx</code> (server component):
import { prisma } from "@/lib/prisma";
export default async function UsersPage() {
const users = await prisma.user.findMany({
orderBy: { createdAt: "desc" },
take: 20,
});
return (
<div>
<h1>Users</h1>
<ul>
{users.map((u) => (
<li key={u.id}>
{u.email}
</li>
))}
</ul>
</div>
);
}If you need client-side interactivity, keep Prisma calls in server code and pass data down as props.
Step 9: Environment variables and build behavior
Your Prisma schema uses <code>DATABASE_URL</code>. Common pitfalls:
- Missing <code>DATABASE_URL</code> in production deploy environment
- Different database credentials between local and production
- Running migrations at the wrong time
Step 10: Production tips for Prisma with Next.js
1. Avoid creating Prisma Client per request
Use the <code>lib/prisma.ts</code> helper pattern shown above.
2. Understand database connection pooling
Prisma opens connections based on your database driver. In serverless setups, repeated cold starts can create extra connections. Mitigations:
- Use database providers with good pooling
- Use a connection pooler (for example PgBouncer for PostgreSQL)
3. Run migrations in CI or during deployment
A common workflow:
- Build app
- Apply migrations before or during deploy
- Start the server
4. Generate Prisma Client after schema changes
Locally you run <code>prisma migrate dev</code>. In production builds, ensure Prisma Client generation happens (it usually does via <code>@prisma/client</code>, but confirm your deployment pipeline).
5. Select only what you need
Use <code>select</code> to reduce payload sizes and improve performance. Example:
await prisma.user.findMany({
select: { id: true, email: true },
});Example: End-to-end create user API route
Create <code>app/api/users/route.ts</code> with both GET and POST.
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function GET() {
const users = await prisma.user.findMany({
orderBy: { createdAt: "desc" },
});
return NextResponse.json({ users });
}
export async function POST(request: Request) {
const body = await request.json();
const { email, name } = body as { email: string; name?: string };
if (!email) {
return NextResponse.json(
{ error: "email is required" },
{ status: 400 },
);
}
const user = await prisma.user.create({
data: { email, name },
});
return NextResponse.json({ user }, { status: 201 });
}Common mistakes
- Importing Prisma into Client Components
- Forgetting to add <code>DATABASE_URL</code>
- Not running migrations after changing <code>schema.prisma</code>
- Using Prisma Client from shared files that might be imported by the browser
Next steps
- Add validation (for example zod) before writing to the database
- Add pagination for list endpoints
- Add error handling for unique constraint violations
- Add a simple seed script for local development