Data validation prevents bugs and security issues. This guide covers validation strategies using Zod and other tools.
Zod Schema Validation#
Basic Schemas#
1import { z } from 'zod';
2
3const UserSchema = z.object({
4 id: z.string().uuid(),
5 email: z.string().email(),
6 name: z.string().min(2).max(100),
7 age: z.number().int().min(0).max(150).optional(),
8 role: z.enum(['user', 'admin', 'moderator']),
9 createdAt: z.date(),
10});
11
12type User = z.infer<typeof UserSchema>;
13
14// Validation
15const result = UserSchema.safeParse(input);
16if (!result.success) {
17 console.error(result.error.issues);
18}Custom Validators#
1const PasswordSchema = z.string()
2 .min(8)
3 .max(100)
4 .refine(
5 (password) => /[A-Z]/.test(password),
6 { message: 'Password must contain uppercase letter' }
7 )
8 .refine(
9 (password) => /[0-9]/.test(password),
10 { message: 'Password must contain number' }
11 )
12 .refine(
13 (password) => /[!@#$%^&*]/.test(password),
14 { message: 'Password must contain special character' }
15 );
16
17const SlugSchema = z.string()
18 .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Invalid slug format')
19 .max(100);Nested Objects#
1const AddressSchema = z.object({
2 street: z.string(),
3 city: z.string(),
4 state: z.string().length(2),
5 zipCode: z.string().regex(/^\d{5}(-\d{4})?$/),
6 country: z.string().default('US'),
7});
8
9const OrderSchema = z.object({
10 items: z.array(z.object({
11 productId: z.string().uuid(),
12 quantity: z.number().int().positive(),
13 price: z.number().positive(),
14 })).min(1),
15 shippingAddress: AddressSchema,
16 billingAddress: AddressSchema.optional(),
17 total: z.number().positive(),
18});Transformations#
1const InputSchema = z.object({
2 email: z.string().email().toLowerCase(),
3 name: z.string().trim(),
4 tags: z.string().transform(s => s.split(',').map(t => t.trim())),
5 date: z.string().transform(s => new Date(s)),
6});
7
8// Coercion for form data
9const FormSchema = z.object({
10 age: z.coerce.number().int().positive(),
11 active: z.coerce.boolean(),
12 date: z.coerce.date(),
13});Express Middleware#
1function validate<T extends z.ZodSchema>(schema: T) {
2 return (req: Request, res: Response, next: NextFunction) => {
3 const result = schema.safeParse({
4 body: req.body,
5 query: req.query,
6 params: req.params,
7 });
8
9 if (!result.success) {
10 return res.status(400).json({
11 error: 'Validation failed',
12 details: result.error.issues.map(issue => ({
13 path: issue.path.join('.'),
14 message: issue.message,
15 })),
16 });
17 }
18
19 req.validated = result.data;
20 next();
21 };
22}
23
24// Usage
25const CreateUserSchema = z.object({
26 body: UserSchema.omit({ id: true, createdAt: true }),
27});
28
29app.post('/users', validate(CreateUserSchema), async (req, res) => {
30 const user = await createUser(req.validated.body);
31 res.json(user);
32});Sanitization#
1import DOMPurify from 'isomorphic-dompurify';
2
3const ContentSchema = z.object({
4 title: z.string()
5 .trim()
6 .min(1)
7 .max(200)
8 .transform(s => DOMPurify.sanitize(s)),
9
10 html: z.string()
11 .transform(s => DOMPurify.sanitize(s, {
12 ALLOWED_TAGS: ['p', 'b', 'i', 'a', 'ul', 'li'],
13 ALLOWED_ATTR: ['href'],
14 })),
15});Database Validation#
1// Prisma with Zod
2import { Prisma } from '@prisma/client';
3
4const createUser = async (input: unknown) => {
5 const validated = UserSchema.parse(input);
6
7 return prisma.user.create({
8 data: validated,
9 });
10};Validate at API boundaries, use type-safe schemas, and sanitize user-generated content.