Overview
All API inputs must be validated with Zod schemas using validateRequest(). This ensures type safety at runtime and provides clear error messages.
What it is
Zod schema validation utilities that parse request bodies, query params, and route params with automatic error formatting.
Why we use it
Type safety at runtime, clear validation errors, consistent error responses, and protection against malformed input.
When to use
Every API route that accepts input. Never trust unvalidated data.
Key Features
- validateRequest() for body validation
- validateSearchParams() and validateParams()
- Common schema patterns (email, uuid, pagination)
- Schema transforms and refinements
Quick Start
Basic Validation
Validating a POST request body.
// Basic body validation
import { validateRequest } from '@/lib/validation';
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
});
export async function POST(request: Request) {
const body = await request.json();
const validation = validateRequest(body, CreateUserSchema);
if (!validation.success) {
return validation.error; // Returns formatted 400 response
}
const { email, name } = validation.data; // Type-safe!
// ...
}Patterns
Body Validation
Validating JSON request bodies.
// validateRequest for body validation
import { validateRequest } from '@/lib/validation';
const validation = validateRequest(body, schema);
if (!validation.success) return validation.error;
// validation.data is fully typed based on your schema
const { field1, field2 } = validation.data;Query Parameters
Validating URL search parameters.
// validateSearchParams for query strings
import { validateSearchParams } from '@/lib/validation';
const ListSchema = z.object({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(1).max(100).default(20),
search: z.string().optional(),
});
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const validation = validateSearchParams(searchParams, ListSchema);
if (!validation.success) return validation.error;
const { page, limit, search } = validation.data;
// ...
}Nested Objects
Validating complex nested structures.
// Nested object validation
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
country: z.string(),
});
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
address: AddressSchema,
tags: z.array(z.string()).default([]),
});Array Validation
Validating arrays with item schemas.
// Array validation with item schemas
const BulkCreateSchema = z.object({
items: z.array(
z.object({
name: z.string().min(1),
quantity: z.number().int().positive(),
})
).min(1).max(100),
});
// With transforms
const IdsSchema = z.object({
ids: z.string().transform(s => s.split(',')).pipe(
z.array(z.string().uuid())
),
});Watch Out
Using request data without validation
Don't
// Trusting unvalidated input
const { userId, role } = await request.json();
await updateUser(userId, { role }); // Dangerous!Do
// Always validate input
const validation = validateRequest(body, UpdateSchema);
if (!validation.success) return validation.error;
const { userId, role } = validation.data;
await updateUser(userId, { role }); // Safe!Making schemas too permissive with optional fields
Don't
// Overly permissive schema
const schema = z.object({
data: z.any(), // Accepts anything!
options: z.record(z.unknown()),
});Do
// Explicit schema
const schema = z.object({
data: z.object({
name: z.string(),
value: z.number(),
}),
options: z.object({
notify: z.boolean().default(false),
}),
});- Not providing clear error messages
- Not using Zod type inference for handlers