Zod: The Complete Validation Guide for Next.js and TypeScript
Zod is the standard for runtime validation in modern TypeScript apps. Here's everything you need — from basic schemas to form validation to API input parsing.
Why Zod
TypeScript types are erased at runtime. A string type annotation doesn't prevent someone from sending a number to your API route.
Zod validates at runtime and infers TypeScript types from the same schema — one source of truth for both concerns:
import { z } from 'zod';
const UserSchema = z.object({
email: z.string().email(),
age: z.number().int().min(18),
});
// TypeScript type inferred from schema — no duplicate definition
type User = z.infer<typeof UserSchema>;
// Runtime validation
const result = UserSchema.safeParse({ email: 'bad', age: 15 });
if (!result.success) {
console.log(result.error.flatten());
// { fieldErrors: { email: ['Invalid email'], age: ['Number must be >= 18'] } }
}
Core Primitives
z.string() // string
z.number() // number
z.boolean() // boolean
z.date() // Date object
z.literal('admin') // exact value
z.enum(['a', 'b', 'c']) // one of set
z.null() // null
z.undefined() // undefined
z.unknown() // unknown (accepts anything, infers unknown)
z.any() // any (escape hatch)
String Validators
z.string().min(3) // minimum length
z.string().max(100) // maximum length
z.string().email() // email format
z.string().url() // URL format
z.string().uuid() // UUID format
z.string().regex(/^[a-z]+$/) // custom regex
z.string().trim() // transform: strip whitespace
z.string().toLowerCase() // transform: lowercase
z.string().optional() // string | undefined
z.string().nullable() // string | null
z.string().default('value') // default if undefined
Objects and Arrays
// Object schema
const ProfileSchema = z.object({
name: z.string().min(1),
bio: z.string().max(500).optional(),
role: z.enum(['user', 'admin', 'moderator']),
});
// Nested objects
const OrderSchema = z.object({
user: ProfileSchema,
items: z.array(z.object({
productId: z.string().cuid(),
quantity: z.number().int().positive(),
})),
total: z.number().positive(),
});
// Partial (all fields optional)
const UpdateProfileSchema = ProfileSchema.partial();
// Pick specific fields
const NameOnlySchema = ProfileSchema.pick({ name: true });
// Omit fields
const NoRoleSchema = ProfileSchema.omit({ role: true });
Validating API Route Input
// app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
const ContactSchema = z.object({
name: z.string().min(1, 'Name is required').max(100),
email: z.string().email('Invalid email address'),
message: z.string().min(10, 'Message too short').max(2000),
});
export async function POST(req: NextRequest) {
const body = await req.json();
const parsed = ContactSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ errors: parsed.error.flatten().fieldErrors },
{ status: 400 }
);
}
// parsed.data is typed as { name: string, email: string, message: string }
const { name, email, message } = parsed.data;
// ... handle submission
}
Form Validation with React Hook Form
npm install react-hook-form @hookform/resolvers zod
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const FormSchema = z.object({
email: z.string().email('Enter a valid email'),
password: z.string().min(8, 'At least 8 characters'),
});
type FormValues = z.infer<typeof FormSchema>;
export function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormValues>({
resolver: zodResolver(FormSchema),
});
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
<input type='password' {...register('password')} />
{errors.password && <p>{errors.password.message}</p>}
<button type='submit'>Login</button>
</form>
);
}
Validating Environment Variables
// lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
AUTH_SECRET: z.string().min(32),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
ANTHROPIC_API_KEY: z.string().startsWith('sk-ant-'),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});
// Throws at startup if any variable is missing or malformed
export const env = envSchema.parse(process.env);
Pre-Configured in the Starter Kit
The AI SaaS Starter Kit uses Zod throughout — API route validation, form schemas, env var validation, and Prisma input sanitization.
Atlas — building at whoffagents.com
Top comments (0)