What Is Zod?
Zod is a TypeScript-first schema validation library. You define a schema once, use it to validate data at runtime, and automatically get the TypeScript type inferred from it — no need to define both a type and a validator separately.
import { z } from 'zod'
const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
name: z.string().min(2).max(100),
role: z.enum(['admin', 'user', 'moderator']),
createdAt: z.date().optional(),
})
// Infer the TypeScript type automatically
type User = z.infer<typeof UserSchema>
// type User = { id: number; email: string; name: string; role: 'admin' | 'user' | 'moderator'; createdAt?: Date }
Common Schema Patterns
// Primitive types
z.string()
z.number().int().positive()
z.boolean()
z.date()
z.undefined()
z.null()
// String refinements
z.string().min(8).max(100).regex(/^[a-z]+$/)
z.string().email()
z.string().url()
z.string().uuid()
z.string().trim().toLowerCase()
// Number refinements
z.number().min(0).max(100)
z.number().int()
z.number().finite()
// Arrays and objects
z.array(z.string()).min(1).max(10)
z.record(z.string(), z.number()) // { [key: string]: number }
// Union and intersection
z.union([z.string(), z.number()])
z.string().or(z.number()) // shorthand
AdminSchema.merge(UserSchema) // intersection
Parsing and Safe Parsing
const data = { id: 1, email: '[email protected]', name: 'Alice', role: 'admin' }
// parse() — throws ZodError on invalid data
const user = UserSchema.parse(data)
// safeParse() — returns { success, data } or { success: false, error }
const result = UserSchema.safeParse(data)
if (result.success) {
console.log(result.data.email)
} else {
console.error(result.error.format())
// { email: { _errors: ['Invalid email'] }, ... }
}
// parseAsync() — for schemas with async refinements
const user = await UserSchema.parseAsync(data)
Validating Environment Variables
// env.ts — validate at app startup, not at runtime
import { z } from 'zod'
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
REDIS_URL: z.string().url().optional(),
API_SECRET: z.string().min(32),
})
export const env = EnvSchema.parse(process.env)
// TypeScript now knows env.PORT is a number, env.DATABASE_URL is a string, etc.
Form Validation with React Hook Form
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const SignupSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
})
type SignupForm = z.infer<typeof SignupSchema>
function SignupForm() {
const { register, handleSubmit, formState: { errors } } = useForm<SignupForm>({
resolver: zodResolver(SignupSchema),
})
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<button type="submit">Sign Up</button>
</form>
)
}
Frequently Asked Questions
How does Zod compare to Yup or Joi?
Zod is TypeScript-first — types are inferred from schemas rather than defined separately. Yup and Joi predate TypeScript and have bolted-on type support. Zod also has better performance and a cleaner API for complex transformations.
Can Zod validate at the API route level?
Yes. In Next.js App Router, call Schema.parse(await request.json()) at the top of your route handler. The ZodError can be caught and returned as a 400 response with structured field errors.
Does Zod work in the browser?
Yes. Zod is framework-agnostic and has no Node.js dependencies. It's commonly used in browser forms, React Native apps, and Cloudflare Workers.