Валидация через Zod
Zod — библиотека для валидации с выводом типов.
Установка
npm install zod
Базовый пример
import { z } from 'zod';
const schema = z.object({
title: z.string().min(3).max(200),
body: z.string().min(10),
published: z.boolean().optional().default(false),
});
export async function POST(request: Request) {
const json = await request.json();
const result = schema.safeParse(json);
if (!result.success) {
return Response.json(
{ errors: result.error.flatten().fieldErrors },
{ status: 422 }
);
}
const post = await db.post.create({ data: result.data });
return Response.json(post, { status: 201 });
}
safeParse не бросает — возвращает { success, data | error }.
Типы из схемы
type PostInput = z.infer<typeof schema>;
Одно определение — и валидация, и тип.
Типы правил
const schema = z.object({
// строки
title: z.string().min(3, 'Минимум 3').max(200, 'Максимум 200'),
email: z.string().email('Некорректный email'),
url: z.string().url('Некорректный URL'),
uuid: z.string().uuid(),
// числа
age: z.number().int().min(18).max(120),
price: z.number().positive(),
// булевы
published: z.boolean().default(false),
// даты
createdAt: z.date(),
dateString: z.string().datetime(),
// enum
status: z.enum(['draft', 'published', 'archived']),
// массивы
tags: z.array(z.string()).min(1).max(10),
// опциональные
bio: z.string().optional(),
phone: z.string().nullable(),
// вложенные
author: z.object({
name: z.string(),
email: z.string().email(),
}),
});
Трансформации
const schema = z.object({
published: z.string().transform((v) => v === 'true'),
age: z.string().transform(Number),
tags: z.string().transform((v) => v.split(',')),
});
Или через coerce:
const schema = z.object({
page: z.coerce.number().int().min(1).default(1),
published: z.coerce.boolean().default(false),
});
Refine
Кастомная проверка:
const schema = z.object({
password: z.string().min(8),
confirm: z.string(),
}).refine((data) => data.password === data.confirm, {
message: 'Пароли не совпадают',
path: ['confirm'],
});
superRefine для сложных:
const schema = z.object({
start: z.date(),
end: z.date(),
}).superRefine((data, ctx) => {
if (data.end <= data.start) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Конец должен быть после начала',
path: ['end'],
});
}
});
Форматирование ошибок
const result = schema.safeParse(json);
if (!result.success) {
const errors = result.error.flatten().fieldErrors;
// { title: ['Минимум 3'], body: ['Минимум 10'] }
return Response.json({ errors }, { status: 422 });
}
Другие форматы:
flatten()—{ fieldErrors, formErrors }format()— вложенная структураissues— массив всех ошибок
Query-параметры
const QuerySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
search: z.string().optional(),
});
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const result = QuerySchema.safeParse(Object.fromEntries(searchParams));
if (!result.success) {
return Response.json({ errors: result.error.flatten() }, { status: 422 });
}
const { page, limit, search } = result.data;
// ...
}
Расширение схем
const BaseSchema = z.object({
title: z.string().min(3),
});
const CreateSchema = BaseSchema.extend({
body: z.string().min(10),
});
const UpdateSchema = BaseSchema.partial();
Pick и Omit
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
password: z.string().min(8),
});
const PublicUserSchema = UserSchema.omit({ password: true });
const AuthSchema = UserSchema.pick({ email: true, password: true });
Итоги
safeParseне бросаетz.inferдаёт типrefineдля кастомных проверокcoerceдля приведенияflattenдля ошибок- Расширение через
extend,pick,omit