Серверная валидация форм является критическим слоем защиты данных, поступающих от клиента, независимо от наличия клиентской проверки. Она обеспечивает целостность, предсказуемость и безопасность входящих данных, снижая риск некорректных операций в бизнес-логике и взаимодействии с базой данных.
Подход на основе схем позволяет описывать структуру данных декларативно. В рамках этого подхода Zod представляет собой библиотеку, ориентированную на строгую типизацию и проверку данных в рантайме с синхронизацией типов TypeScript.
Схема описывает форму объекта, его поля, типы и дополнительные ограничения:
import { z } from "zod";
const userSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
age: z.number().int().positive(),
});
Схема становится единственным источником правды для структуры данных, поступающих в систему.
Zod предоставляет набор примитивных валидаторов:
z.string()z.number()z.boolean()z.date()z.array()Дополнительные ограничения накладываются цепочками методов:
const schema = z.object({
username: z.string().min(3).max(20),
score: z.number().min(0).max(100),
});
Каждое ограничение добавляет условие, проверяемое в рантайме.
Сервер часто получает данные в виде строк, даже если ожидаются числа или даты. Для устранения этого несоответствия используется преобразование:
const schema = z.object({
age: z.coerce.number(),
createdAt: z.coerce.date(),
});
Механизм coercion выполняет приведение типов до валидации, снижая количество ошибок парсинга.
Метод parse выбрасывает исключение при ошибке, тогда как
safeParse возвращает структурированный результат:
const result = userSchema.safeParse(data);
if (!result.success) {
console.log(result.error);
}
Структура результата:
success: true — данные валидныsuccess: false — содержит объект ошибки
ZodErrorZodError содержит детализированную информацию о каждом
нарушении:
Форматирование ошибок:
const formatted = result.error.format();
или более плоский вид:
const flat = result.error.flatten();
Это позволяет удобно возвращать ошибки клиенту в виде JSON.
Сложные формы часто содержат вложенные объекты и массивы:
const schema = z.object({
user: z.object({
name: z.string(),
contacts: z.array(
z.object({
type: z.string(),
value: z.string(),
})
),
}),
});
Валидация проходит рекурсивно, сохраняя структуру путей ошибок.
При обработке PATCH-запросов используется частичная валидация:
const updateSchema = userSchema.partial();
Также применяются операции выбора и исключения полей:
const publicUserSchema = userSchema.omit({
password: true,
});
и
const loginSchema = userSchema.pick({
email: true,
password: true,
});
Схемы могут задавать дефолтные значения:
const schema = z.object({
role: z.string().default("user"),
});
Если поле отсутствует, оно автоматически дополняется.
После валидации данные могут трансформироваться:
const schema = z.string().transform((val) => val.trim().toLowerCase());
Трансформация выполняется после успешной проверки.
Некоторые проверки требуют обращения к внешним ресурсам:
const schema = z.string().refine(async (email) => {
return await isEmailAvailable(email);
}, {
message: "Email уже используется",
});
Асинхронные схемы требуют использования parseAsync или
safeParseAsync.
Валидация входящих запросов часто выполняется на уровне middleware.
app.post("/users", async (req, res) => {
const result = userSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json(result.error.flatten());
}
// result.data содержит валидные данные
});
fastify.post("/users", async (request, reply) => {
const data = userSchema.parse(request.body);
return data;
});
Fastify также поддерживает схемы на уровне маршрутов, но Zod используется как независимый слой валидации.
export default function handler(req, res) {
const result = userSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json(result.error.format());
}
res.status(200).json(result.data);
}
Одним из ключевых преимуществ схемного подхода является автоматическое получение типов:
type User = z.infer<typeof userSchema>;
Это устраняет дублирование типов между runtime-валидацией и компиляцией TypeScript.
Схемы могут комбинироваться:
const baseSchema = z.object({
id: z.string(),
});
const extendedSchema = baseSchema.extend({
name: z.string(),
});
или объединяться:
const merged = z.intersection(schemaA, schemaB);
Валидация часто дополняется очисткой входных значений:
const schema = z.object({
comment: z.string().transform((v) => v.trim()),
});
Это снижает риск хранения неконсистентных данных.
Различие между отсутствующим значением и null фиксируется явно:
const schema = z.object({
middleName: z.string().optional(),
nickname: z.string().nullable(),
});
optional — поле может отсутствоватьnullable — поле может быть nullСложные формы могут требовать зависимых правил:
const schema = z.object({
type: z.enum(["admin", "user"]),
permissions: z.array(z.string()).optional(),
}).refine((data) => {
if (data.type === "admin") return true;
return data.permissions !== undefined;
});
При росте проекта схемы выносятся в отдельные модули, формируя слой контрактов данных между клиентом и сервером. Такой слой снижает связность бизнес-логики и упрощает сопровождение API.
Стабильность структуры данных становится независимой от реализации контроллеров и сервисов, а проверка входящих данных перемещается в строго определённую точку обработки запроса.