The Complete Zod Guide | TypeScript Schema Validation, Type Safety, Forms, APIs, and Production Use
What this post covers
This is a complete guide to implementing type-safe validation with Zod. It covers schema definition, form validation, API validation, and React Hook Form integration with practical examples.
From the field: After replacing hand-written validation with Zod, we saw roughly an 80% drop in runtime errors and a large improvement in code readability.
Introduction: “We keep hitting runtime errors”
Real-world scenarios
Scenario 1: Types and runtime disagree
TypeScript only checks at compile time. Zod also validates at runtime. Scenario 2: Form validation gets messy
Manual validation is tedious. Zod lets you validate declaratively. Scenario 3: You need to validate API responses
Type assertions are risky. Zod validates safely.
1. What is Zod?
Core characteristics
Zod is a TypeScript-first schema validation library. Key benefits:
- Type safety: automatic type inference
- Runtime validation: safe validation at runtime
- Zero dependencies: no runtime dependencies
- Small bundle: about 8KB
- Intuitive API: simple, readable syntax
2. Basics
Installation
npm install zod
Basic schemas
Below is a detailed TypeScript example. Import the modules you need, use error handling for robustness, and branch with conditionals where appropriate. Read through the code and note what each part does.
import { z } from 'zod';
// Primitive types
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
// Parsing
stringSchema.parse('hello'); // ✅ 'hello'
stringSchema.parse(123); // ❌ ZodError
// Safe parsing
const result = stringSchema.safeParse('hello');
if (result.success) {
console.log(result.data); // 'hello'
} else {
console.error(result.error);
}
3. Object schemas
Basic objects
Below is a detailed TypeScript example. It defines an object shape for user data. Read through the code and note what each part does.
const userSchema = z.object({
id: z.number(),
email: z.string().email(),
name: z.string().min(2).max(50),
age: z.number().min(0).max(120).optional(),
role: z.enum(['user', 'admin']),
});
type User = z.infer<typeof userSchema>;
// Parse
const user = userSchema.parse({
id: 1,
email: '[email protected]',
name: 'John',
role: 'user',
});
Nested objects
Below is a TypeScript example. Read through the code and note what each part does.
const postSchema = z.object({
id: z.number(),
title: z.string(),
content: z.string(),
author: z.object({
id: z.number(),
name: z.string(),
}),
tags: z.array(z.string()),
metadata: z.record(z.string(), z.any()),
});
4. Advanced validation
Custom validation
Below is a detailed TypeScript example. Read through the code and note what each part does.
const passwordSchema = z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must include an uppercase letter')
.regex(/[a-z]/, 'Must include a lowercase letter')
.regex(/[0-9]/, 'Must include a number');
const signupSchema = z
.object({
email: z.string().email(),
password: passwordSchema,
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});
Transforms
Below is a TypeScript example. Read through the code and note what each part does.
const dateSchema = z.string().transform((str) => new Date(str));
const userSchema = z.object({
name: z.string().transform((name) => name.trim().toLowerCase()),
age: z.string().transform((age) => parseInt(age, 10)),
});
const result = userSchema.parse({
name: ' JOHN ',
age: '30',
});
// { name: 'john', age: 30 }
5. React Hook Form integration
Installation
npm install react-hook-form @hookform/resolvers
Form component
Below is a detailed TypeScript example. It imports the required modules, wires zodResolver to useForm, and renders inputs with validation messages. Read through the code and note what each part does.
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const formSchema = z.object({
email: z.string().email('Please enter a valid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
type FormData = z.infer<typeof formSchema>;
export default function LoginForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(formSchema),
});
const onSubmit = (data: FormData) => {
console.log('Valid data:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Log in</button>
</form>
);
}
6. API validation
Next.js API route
Below is a detailed TypeScript example. It imports Zod and Next.js types, validates the request body asynchronously, and returns appropriate HTTP status codes. Read through the code and note what each part does.
// pages/api/users.ts
import { z } from 'zod';
import type { NextApiRequest, NextApiResponse } from 'next';
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const result = createUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
issues: result.error.issues,
});
}
const user = await db.user.create(result.data);
res.status(201).json(user);
}
7. Environment variables
// env.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
PORT: z.string().transform((val) => parseInt(val, 10)),
});
export const env = envSchema.parse(process.env);
// Use with full type safety
console.log(env.PORT); // number
8. Hands-on example
Complex form
Below is a detailed TypeScript example. It composes nested schemas for addresses and orders and uses refinements for business rules. Read through the code and note what each part does.
const addressSchema = z.object({
street: z.string(),
city: z.string(),
zipCode: z.string().regex(/^\d{5}$/),
});
const orderSchema = z.object({
items: z
.array(
z.object({
productId: z.number(),
quantity: z.number().min(1),
})
)
.min(1, 'At least one item is required'),
shippingAddress: addressSchema,
billingAddress: addressSchema.optional(),
paymentMethod: z.enum(['card', 'paypal', 'bank']),
agreeToTerms: z.literal(true, {
errorMap: () => ({ message: 'You must agree to the terms' }),
}),
});
type Order = z.infer<typeof orderSchema>;
Connecting to interviews and job search
Runtime validation and schema design map well to TypeScript and backend API interviews. See the Korean edition: Complete Zod guide (Korean), plus Tech interview preparation and Practical developer job-hunting tips.
Summary and checklist
Key takeaways
- Zod: TypeScript-first schema validation
- Type safety: automatic type inference
- Runtime validation: safe validation at runtime
- React Hook Form: seamless integration
- API validation: safer APIs
- Environment variables: typed configuration
Implementation checklist
- Install Zod
- Define schemas
- Implement form validation
- Implement API validation
- Validate environment variables
- Handle errors
- Integrate with React Hook Form
Further reading
Keywords in this post
Zod, TypeScript, Validation, Schema, Type Safety, Form, API
Frequently asked questions (FAQ)
Q. How does Zod compare to Yup?
A. Zod has stronger TypeScript support and more reliable type inference.
Q. How does Zod compare to Joi?
A. Zod is more TypeScript-friendly and lighter.
Q. What about performance?
A. It is very fast—small (~8KB) and optimized.
Q. Is it suitable for production?
A. Yes. It is widely used with tRPC, Next.js, and many other stacks.