Валидация данных: Joi, Zod
Joi и Zod — JavaScript-библиотеки схем для декларативной валидации данных на сервере (и клиенте), задающие правила через цепочки методов или типы TypeScript.
Зачем нужно
HTML5-валидация и браузерная проверка форм — первый рубеж, но обойти их легко (прямой POST-запрос). Серверная валидация через Joi или Zod — обязательный второй рубеж: все данные от пользователя считаются ненадёжными. Zod добавляет TypeScript-типы из схемы автоматически, что устраняет дублирование типов и интерфейсов. Joi — более зрелый вариант для Node.js-бэкенда.
Где используется
- Серверная валидация API-запросов (Express, Fastify, NestJS)
- Валидация переменных окружения (
.envпроверка при старте) - Zod: вместе с React Hook Form, tRPC, Next.js App Router
- Joi: в Hapi.js (встроена), Express middleware
- Парсинг и типизация ответов от внешних API
Zod — схемы с TypeScript-типами
import { z } from 'zod';
// Определение схемы
const registerSchema = z.object({
name: z.string.min(2, 'Имя не менее 2 символов').max(50),
email: z.string.email('Некорректный email'),
password: z.string.min(8, 'Пароль не менее 8 символов'),
age: z.number.int.min(18, 'Только для взрослых').optional,
role: z.enum(['user', 'admin']).default('user'),
});
// Автоматический TypeScript-тип
type RegisterInput = z.infer<typeof registerSchema>;
// Парсинг (throw при ошибке)
const data = registerSchema.parse(req.body);
// Безопасный парсинг (без throw)
const result = registerSchema.safeParse(req.body);
if (!result.success) {
const errors = result.error.flatten.fieldErrors;
return res.status(400).json({ errors });
}
const validData = result.data;
Zod — вложенные схемы и массивы
const addressSchema = z.object({
city: z.string,
street: z.string,
});
const orderSchema = z.object({
items: z.array(z.object({
productId: z.string.uuid,
quantity: z.number.int.positive,
})).min(1, 'Корзина пустая'),
address: addressSchema,
delivery: z.enum(['courier', 'pickup']),
});
Joi — схемы для Node.js
const Joi = require('joi');
const registerSchema = Joi.object({
name: Joi.string.min(2).max(50).required,
email: Joi.string.email.required,
password: Joi.string.min(8).required,
age: Joi.number.integer.min(18).optional,
role: Joi.string.valid('user', 'admin').default('user'),
});
// Валидация
const { error, value } = registerSchema.validate(req.body, {
abortEarly: false, // собрать все ошибки, не только первую
});
if (error) {
const messages = error.details.map(d => d.message);
return res.status(400).json({ errors: messages });
}
// value — очищенные и приведённые данные
Express middleware с Joi
function validate(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, { abortEarly: false });
if (error) {
return res.status(400).json({
errors: error.details.map(d => ({ field: d.path[0], message: d.message }))
});
}
req.body = value; // заменить тело очищенными данными
next;
};
}
app.post('/register', validate(registerSchema), registerController);
Сравнение Zod и Joi
| Критерий | Zod | Joi |
|---|---|---|
| TypeScript-интеграция | Встроена (типы из схемы) | Базовая (отдельные типы) |
| Размер бандла | Меньше (~13 кБ) | Больше (~30 кБ) |
| Экосистема | tRPC, React Hook Form, Prisma | Hapi, Express, NestJS |
| API | Цепочки TypeScript | Цепочки JS |
| Кастомные валидаторы | .refine, .superRefine |
.custom |
Частые ошибки
| Ошибка | Почему плохо | Как правильно |
|---|---|---|
| Только HTML5-валидация без серверной | Обходится прямым запросом | Всегда валидируй на сервере |
abortEarly: true (по умолчанию в Joi) |
Пользователь видит только первую ошибку | abortEarly: false |
| Не очищать входные данные | XSS, инъекции | Используй value из Joi или data из Zod |
| Дублировать Zod-схему и TypeScript-тип | Рассинхрон | type T = z.infer<typeof schema> |