Building Type-Safe API Routes in Next.js with Zod
How to build fully type-safe API route handlers in Next.js using Zod for runtime validation that syncs with your TypeScript types.
TypeScript gives you compile-time safety, but your API routes receive unknown data at runtime. A user can POST anything they want — and as SomeType assertions won't save you in production. Zod bridges this gap: runtime validation that automatically infers TypeScript types. Here's how to build bulletproof API routes.
The Problem with Unvalidated Routes
This looks safe but isn't:
// DANGEROUS: trusting unknown input
export async function POST(request: Request) {
const body = await request.json() as CreatePostInput; // runtime lie
await prisma.post.create({ data: body }); // SQL injection? Missing fields?
}
TypeScript doesn't validate runtime data. You need a schema validator. Zod is the best choice for Next.js because it's tiny (12KB), has zero dependencies, and its type inference is exceptional.
Setting Up Zod Schemas
Define your schemas once and derive both runtime validators and TypeScript types:
// lib/validations/post.ts
import { z } from 'zod';
export const createPostSchema = z.object({
title: z
.string()
.min(3, 'Title must be at least 3 characters')
.max(200, 'Title must be under 200 characters'),
content: z.string().min(50, 'Content must be at least 50 characters'),
summary: z.string().max(300).optional(),
categorySlug: z.string().optional(),
tags: z.array(z.string()).max(5, 'Maximum 5 tags').optional(),
status: z.enum(['DRAFT', 'PUBLISHED']).default('DRAFT'),
coverImageUrl: z.string().url('Invalid URL').optional(),
});
// Type is automatically inferred — never out of sync
export type CreatePostInput = z.infer<typeof createPostSchema>;
Type-Safe Route Handler
Now your API route validates every request before processing:
// app/api/v1/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createPostSchema } from '@/lib/validations/post';
import { prisma } from '@/lib/prisma';
import { slugify } from '@/lib/utils';
export const dynamic = 'force-dynamic';
export async function POST(request: NextRequest) {
const body = await request.json();
const parsed = createPostSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{
error: 'Validation failed',
details: parsed.error.flatten().fieldErrors,
},
{ status: 400 }
);
}
// parsed.data is fully typed as CreatePostInput
const { title, content, summary, tags, status, categorySlug } = parsed.data;
const post = await prisma.blogPost.create({
data: {
title,
slug: slugify(title),
content,
summary: summary ?? title,
status,
articleType: 'COMMUNITY',
category: categorySlug
? { connect: { slug: categorySlug } }
: undefined,
},
});
return NextResponse.json(post, { status: 201 });
}
If validation fails, the client gets structured error messages with field-level details. If it passes, parsed.data is fully typed — no assertions needed.
Composing Schemas
Zod schemas compose naturally. Need an update schema that makes everything optional? Use .partial(). Need query parameters? Define a separate schema:
// Reuse the create schema for updates
export const updatePostSchema = createPostSchema.partial();
// Query parameter validation
export const postQuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(10),
status: z.enum(['DRAFT', 'PUBLISHED']).optional(),
search: z.string().optional(),
});
Error Handling Pattern
Create a helper function to reduce boilerplate across all your routes:
import { ZodSchema, ZodError } from 'zod';
export function validateRequest<T>(
schema: ZodSchema<T>,
data: unknown
): { success: true; data: T } | { success: false; response: NextResponse } {
const result = schema.safeParse(data);
if (!result.success) {
return {
success: false,
response: NextResponse.json(
{ error: 'Validation failed', details: result.error.flatten().fieldErrors },
{ status: 400 }
),
};
}
return { success: true, data: result.data };
}
Now every route handler is three lines of validation code. The types flow through automatically, your runtime is protected, and your error responses are consistent. That's the power of Zod with Next.js.
Admin
Cal.com
Open source scheduling — tự host booking system, thay thế Calendly. Free & privacy-first.
Bình luận (0)
Đăng nhập để bình luận
Chưa có bình luận nào. Hãy là người đầu tiên!