Why Prisma Needs Zod
Prisma gives you TypeScript types for your database models — but those types only exist at compile time. When an API request arrives, it's untyped data from the outside world. Zod bridges this gap: it validates the incoming data at runtime so that only valid, correctly-typed data reaches your Prisma queries.
The combination of Prisma (database layer) + Zod (validation layer) + TypeScript (type safety) is the standard stack for modern TypeScript backends.
From Prisma Model to Zod Schema
Given a Prisma model, here's how to create the corresponding Zod schema:
// Prisma schema (schema.prisma)
model User {
id Int @id @default(autoincrement())
name String
email String @unique
role Role @default(USER)
createdAt DateTime @default(now())
posts Post[]
}
enum Role {
ADMIN
USER
}// Zod schema (user.schema.ts)
import { z } from 'zod';
export const RoleSchema = z.enum(['ADMIN', 'USER']);
export const UserSchema = z.object({
id: z.number().int(),
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email format'),
role: RoleSchema,
createdAt: z.date(),
});
// For create operations (no id, no createdAt)
export const CreateUserSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email format'),
role: RoleSchema.optional().default('USER'),
});
// For update operations (all fields optional)
export const UpdateUserSchema = CreateUserSchema.partial();
export type User = z.infer<typeof UserSchema>;
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>;Use our Prisma to Zod converter to automatically generate the base schemas from your Prisma models.
API Route Validation Pattern
Here's a complete Next.js API route using Prisma + Zod:
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { CreateUserSchema } from '@/schemas/user.schema';
export async function POST(request: NextRequest) {
const body: unknown = await request.json();
const result = CreateUserSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ errors: result.error.flatten().fieldErrors },
{ status: 400 }
);
}
const user = await prisma.user.create({
data: result.data, // fully typed, matches Prisma's create input
});
return NextResponse.json(user, { status: 201 });
}tRPC + Prisma + Zod
tRPC makes the Prisma + Zod combination even more powerful. Zod schemas become the input validators for your tRPC procedures:
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';
import { CreateUserSchema } from '@/schemas/user.schema';
import { prisma } from '@/lib/prisma';
export const userRouter = router({
create: publicProcedure
.input(CreateUserSchema)
.mutation(async ({ input }) => {
// input is fully typed as CreateUserInput
return prisma.user.create({ data: input });
}),
getById: publicProcedure
.input(z.object({ id: z.number().int() }))
.query(async ({ input }) => {
return prisma.user.findUnique({ where: { id: input.id } });
}),
});Using prisma-zod-generator
For larger projects, consider the prisma-zod-generator package which auto-generates Zod schemas every time you run prisma generate:
generator zod {
provider = "prisma-zod-generator"
output = "./generated/zod"
}This is the production approach — schemas always stay in sync with your Prisma models without manual maintenance.
