Валидация входящих запросов в серверных приложениях является критическим этапом защиты и стабилизации системы. Любые данные, поступающие извне — тело запроса, параметры URL, query-строка, заголовки — должны рассматриваться как потенциально некорректные или злонамеренные. Ошибки на этом уровне приводят не только к сбоям, но и к уязвимостям: от нарушения бизнес-логики до инъекций и утечек данных.
Zod представляет собой декларативную библиотеку описания и проверки схем данных, ориентированную на строгую типизацию и предсказуемую валидацию. Основная идея заключается в том, что структура данных описывается один раз в виде схемы, после чего используется как для проверки входящих значений, так и для вывода типов TypeScript.
Любой HTTP-запрос в backend-системе состоит из нескольких независимых источников данных:
req.body — тело запроса (POST, PUT, PATCH)req.params — параметры маршрутаreq.query — query-строкаreq.headers — заголовкиКаждый из этих источников имеет собственные особенности:
undefinedТиповая ошибка заключается в предположении, что данные уже имеют нужный формат. В реальности требуется явное описание ожидаемой структуры.
Основной строительный блок — схема (schema). Она описывает тип данных и правила его проверки.
import { z } fr om "zod";
const idSchema = z.string();
const ageSchema = z.number();
const isActiveSchema = z.boolean();
Каждая схема выполняет две функции:
Типичный сценарий — проверка req.body.
const createUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
age: z.number().int().positive()
});
Применение:
const result = createUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json(result.error.flatten());
}
const data = result.data;
Метод safeParse возвращает структурированный
результат:
success: true → данные валидныsuccess: false → содержит объект ошибокИспользование safeParse предпочтительнее
parse, поскольку исключения не прерывают поток
выполнения.
Ошибки Zod имеют детализированную структуру:
{
formErrors: [],
fieldErrors: {
email: ["Invalid email"],
password: ["Too short"]
}
}
Это позволяет напрямую использовать результат для фронтенда без дополнительной обработки.
Параметры URL всегда приходят в виде строк.
const paramsSchema = z.object({
userId: z.string().uuid()
});
Использование:
const parsed = paramsSchema.safeParse(req.params);
При необходимости числового значения используется преобразование:
const paramsSchema = z.object({
userId: z.string().transform(val => Number(val))
});
Query-строка часто содержит опциональные и строковые значения:
const querySchema = z.object({
page: z.string().optional(),
lim it: z.string().optional()
});
Расширенный вариант с преобразованием типов:
const querySchema = z.object({
page: z.string().transform(Number).default("1"),
limit: z.string().transform(Number).default("20")
});
Одной из ключевых возможностей является трансформация значений после валидации:
const schema = z.string().transform(str => str.trim());
В контексте запросов:
const schema = z.object({
tags: z.string().transform(val => val.split(","))
});
Это позволяет одновременно валидировать и нормализовать данные.
HTTP-запросы не несут строгих типов, поэтому часто используется coercion:
const schema = z.object({
age: z.coerce.number()
});
Поведение:
"25" → 25"abc" → ошибкаЭто особенно важно для req.query и
req.params.
const schema = z.object({
name: z.string(),
role: z.string().optional(),
status: z.string().default("active")
});
Поведение:
optional() допускает отсутствие поляdefault() подставляет значение при отсутствииДля сложных API используются вложенные объекты:
const schema = z.object({
user: z.object({
profile: z.object({
firstName: z.string(),
lastName: z.string()
})
})
});
Такая структура обеспечивает строгую типизацию даже для сложных JSON.
const schema = z.object({
tags: z.array(z.string())
});
Валидация каждого элемента происходит автоматически.
Дополнительно:
z.array(z.number().int().positive()).min(1).max(10)
Когда стандартных ограничений недостаточно:
const schema = z.string().refine(val => val.startsWith("usr_"), {
message: "Invalid prefix"
});
Для объектов:
const schema = z.object({
password: z.string(),
confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"]
});
Некоторые проверки требуют обращения к базе данных или внешним сервисам:
const schema = z.string().refine(async (email) => {
const user = await db.users.findByEmail(email);
return !user;
}, {
message: "Email already exists"
});
В этом случае используется parseAsync:
await schema.parseAsync(req.body);
const baseSchema = z.object({
email: z.string().email()
});
const extendedSchema = baseSchema.extend({
password: z.string().min(8)
});
Также возможно объединение:
const schema = z.union([
z.object({ type: z.literal("admin") }),
z.object({ type: z.literal("user") })
]);
Для частичных обновлений:
const updateSchema = createUserSchema.partial();
Для выбора полей:
const schema = createUserSchema.pick({
email: true
});
const validate = (schema) => (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json(result.error.flatten());
}
req.body = result.data;
next();
};
Использование:
app.post("/users", validate(createUserSchema), handler);
Строгая валидация входящих запросов снижает поверхность атак:
Особое значение имеет принцип: данные должны быть приведены к ожидаемой форме до попадания в бизнес-слой.
Схемы выносятся в отдельные модули:
/schemas
user.schema.js
auth.schema.js
const addressSchema = z.object({
city: z.string(),
street: z.string()
});
const userSchema = z.object({
name: z.string(),
address: addressSchema
});
Одно из ключевых преимуществ:
type User = z.infer<typeof userSchema>;
Это устраняет дублирование типов и схем.
const schema = z.object({
name: z.string()
}).strict();
Поведение:
Это критично для публичных API.
Типичный pipeline обработки запроса:
Zod выступает центральным элементом на втором и третьем этапах, формируя гарантированно корректный контракт данных между клиентом и сервером.