Валидация в Node.js: чек-лист для боевого API
Что проверить в слое валидации Node.js-приложения: границы, типы, вложенные объекты, санитизация. Разбор частых дыр и сравнение Zod, Joi и express-validator.

Запрос без валидации — это чужой код, который вы запускаете на своём сервере. req.body.limit без проверки уходит в LIMIT запроса, поле role из тела внезапно повышает пользователя до админа, а массив на 50 000 элементов кладёт event loop на несколько секунд. Ниже — чек-лист того, что должно проверяться в Node.js-приложении, и на чём чаще всего экономят зря. Материал ориентирован на Node.js 20/22 и типичный стек Express/Fastify.
Где вообще должна стоять валидация
Первое правило: валидируйте на границе. Как только данные попали в бизнес-логику, считать их проверенными уже поздно — вы будете дублировать проверки в каждом сервисе или, что хуже, не будете. Граница — это HTTP-обработчик, обработчик очереди, потребитель Kafka, парсер CLI-аргументов.
Второе: валидация — не то же самое, что санитизация. Валидация решает «пропустить или отклонить», санитизация — «привести к безопасному виду». Строку с пробелами по краям вы обычно триммите (санитизация), а вот пустое обязательное поле отклоняете (валидация). Смешивать их в одной функции можно, но держите в голове, что это две разные операции.
Валидированные данные и входные данные — это разные типы. Если после проверки вы всё ещё лезете в req.body, слой валидации сделан наполовину.
Чек-лист: что проверить
Пройдитесь по каждому пункту для каждого эндпоинта, принимающего вход от клиента.
- Присутствие обязательных полей. Отсутствие поля и
null— разные случаи. Решите заранее, считается лиnullвалидным значением или это ошибка. - Тип каждого поля.
req.queryиreq.paramsвсегда приходят строками.?limit=20— это строка"20", а не число. Приводите явно и проверяйте результат приведения. - Границы. Минимум и максимум для чисел, минимальная и максимальная длина для строк, максимальный размер массивов. Массив без верхней границы — это DoS через один запрос.
- Формат. Email, UUID, дата, URL. Не пишите регулярку для email руками — берите проверку из библиотеки.
- Допустимые значения (enum). Поле
statusдолжно быть одним из известного списка, а не произвольной строкой. - Вложенные объекты и массивы объектов. Валидация верхнего уровня без рекурсии по вложенным структурам пропускает мусор внутрь.
- Запрет лишних полей. Клиент прислал
isAdmin: trueвдобавок к ожидаемым полям — если вы делаетеObject.assignв модель, это дыра. Отбрасывайте или отклоняйте неизвестные ключи. - Размер тела запроса целиком. Ограничьте на уровне парсера body, до того как схема начнёт разбирать 10-мегабайтный JSON.
Про приведение типов из query
Самая частая тихая ошибка. parseInt("20abc") вернёт 20, а не ошибку. Number("") вернёт 0, а не NaN. Поэтому после приведения проверяйте Number.isFinite(), а лучше используйте схему, которая умеет coercion явно и предсказуемо.
Три инструмента и чем они отличаются
Ручные if-проверки на третьем эндпоинте превращаются в кашу, которую невозможно переиспользовать и типизировать. Библиотека даёт схему как единый источник правды. Три распространённых варианта под разные ситуации.
| Критерий | Zod | Joi | express-validator |
|---|---|---|---|
| Вывод типа TypeScript | Да, из схемы через z.infer | Нет из коробки | Нет |
| Куда встраивается | Любой слой, фреймворк-агностик | Любой слой | Только Express, через middleware |
| Стиль описания | Схема-объект | Схема-объект, fluent | Цепочки на полях запроса |
| Когда брать | TypeScript-проект, нужен общий тип для входа и модели | Большая экосистема, привычка по hapi | Мелкий Express-проект без TS |
Если проект на TypeScript, Zod обычно выигрывает за счёт того, что схема и тип — одна сущность: вы не рассинхронизируете интерфейс и проверку. Точные имена методов и опции сверяйте с документацией вашей версии — API у этих библиотек между мажорными версиями менялся.
Zod: схема как источник типа
Пример для Zod 3.x. Схема описывает вход, а тип для остального кода выводится из неё автоматически.
import { z } from "zod";
// Схема входа для создания пользователя
const createUserSchema = z.object({
email: z.string().email(),
age: z.number().int().min(18).max(120),
role: z.enum(["user", "editor"]), // произвольную строку не пропустит
tags: z.array(z.string()).max(20), // ограничили размер массива
}).strict(); // лишние ключи в теле — ошибка
// Тип для сервисов выводим из схемы, а не пишем руками
type CreateUserInput = z.infer<typeof createUserSchema>;
function handler(rawBody: unknown) {
const result = createUserSchema.safeParse(rawBody);
if (!result.success) {
// result.error содержит список проблем по полям
return { status: 400, errors: result.error.issues };
}
const input: CreateUserInput = result.data; // здесь данные уже проверены
return { status: 201, input };
}
Ключевое здесь — .strict() (отклоняет неизвестные поля) и safeParse вместо parse: safeParse не бросает исключение, а возвращает объект с флагом успеха, что удобнее в обработчике.
Coercion для query-параметров
Для полей из req.query, которые приходят строками, нужна явная схема с приведением. В Zod 3.x для этого есть z.coerce.
const listQuerySchema = z.object({
// "20" превратится в число 20; нечисловое даст ошибку валидации
limit: z.coerce.number().int().min(1).max(100).default(20),
offset: z.coerce.number().int().min(0).default(0),
});
Так вы избавляетесь от ручных parseInt с проверкой NaN на каждом эндпоинте.
Ошибки, которые находят в проде
- Валидация есть, а лишние поля проходят. Схема без запрета неизвестных ключей (в Zod — без
.strict()) молча пропускает всё лишнее. Если потом делаетеUser.create(data)напрямую — прилетит и то, чего вы не ждали. - Проверили тело, забыли query и params. ID из
req.params.idуходит в запрос к базе без проверки формата. Для целочисленных ID это ещё и повод для лишних ошибок драйвера. - Нет верхней границы на массивы и строки. Один запрос с массивом на десятки тысяч элементов заставляет схему и сериализацию работать секундами, блокируя event loop.
- Приведение типов молчит об ошибке.
Number("")→0,parseInt("12px")→12. Полагаться на них без последующей проверки — источник тихих багов. - Тексты ошибок валидации утекают наружу как есть. Иногда в них попадают имена внутренних полей и структура модели. Наружу отдавайте нормализованный формат, детали — в лог.
Санитизация: чем это отличается от валидации
После того как схема сказала «данные корректны», может понадобиться привести их к безопасному виду перед сохранением или выводом. Тут важно не перепутать задачи:
- Trim и нормализация — на входе. Убрать пробелы, привести email к нижнему регистру.
- Экранирование для вывода в HTML — на выходе, при рендере, а не при сохранении. Экранировать при записи в базу — частая ошибка, из-за которой данные хранятся в искажённом виде.
- Защита от SQL-инъекций — это не задача валидатора вовсе. Используйте параметризованные запросы или ORM. Валидация формата ID не заменяет параметризацию.
Иначе говоря, валидация не защищает от инъекций сама по себе — она снижает поверхность, но экранирование и параметризацию она не отменяет.
Что настроить дальше
Вынесите схемы в отдельный слой и переиспользуйте их: одна схема на вход эндпоинта, тип для сервиса выводится из неё. Настройте единый обработчик, который ловит ошибки валидации и отдаёт клиенту один и тот же формат ответа с кодом 400 — чтобы фронтенд не разбирал десять разных структур ошибок. Ограничьте размер тела на уровне парсера body до схемы. И заведите в логах отдельный счётчик отклонённых по валидации запросов: резкий рост часто означает либо сломанный клиент, либо чей-то перебор параметров.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: Zod — официальная документация, Express — Best Practices: Security (валидация и санитизация входа)
Частые вопросы
Что выбрать, если проект без TypeScript?
А если у меня другая версия библиотеки — примеры сработают?
Нужна ли валидация, если стоит ORM?
Валидация защитит от SQL-инъекций?
Где валидировать в микросервисах — на каждом сервисе или на шлюзе?
Что делать с производительностью, если схем много и они большие?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.