Zod против express-validator: два подхода к валидации в Express
Сравнивает проверку запросов с упором на схему с помощью Zod и промежуточный модуль express-validator, основанный на цепочках, рассматривая процесс настройки, форматирование ошибок и распространённые проблемы.
Обработка ненадежных данных входящего запроса — одна из первых проблем, с которыми должен столкнуться любой API на базе Express, и существует более одного способа её решения: от библиотек, ориентированных на схемы, до более процедурных инструментов проверки с использованием цепочек операций. В этой статье рассматриваются оба подхода, начиная с метода, основанного на схемах и разработанного с использованием Zod.
Проверка запросов с помощью Zod
Сам Express не выполняет никакой проверки входящих данных. Без контроля на уровне границы обработчики маршрутов получают необработанные значения req.body, req.query и req.params в том виде, в котором они пришли: числовые поля, на самом деле являющиеся строками, полей, которых совсем нет, а также данные, форма которых начинает создавать проблемы только тогда, когда они доходят до бизнес-логики.
Zod решает эту проблему, позволяя описывать ожидаемую структуру данных с помощью схем на основе TypeScript. Вы определяете схему один раз, получаете из неё статический тип с помощью z.infer, а затем парсите входящие данные на уровне HTTP-слоя, так что все последующие компоненты получают только валидные данные. Любые данные, не прошедшие проверку, могут привести к ответу HTTP 400 ещё до выполнения кода обработчика.
В приведённых ниже примерах используется Zod 4 (z.email(), z.uuid(), z.coerce), а также промежуточный модуль проверки для Express, утилита для форматирования ошибок и список распространённых проблем.
Предварительные требования
Вам понадобятся Node.js версии 26, Zod 4 (npm i zod) и Express с его типовыми определениями (npm i express и npm i -D @types/express). Старая синтаксис цепочек Zod 3, такая как z.string().email(), всё ещё работает в версии v4, но считается устаревшей — предпочтите новые функции верхнего уровня, показанные ниже.
Определение схем
// schemas.ts
import { z } from 'zod';
export const createUserSchema = z.object({
email: z.email(),
name: z.string().min(1).max(100),
age: z.number().int().min(0).max(150).optional()
});
export type CreateUserInput = z.infer<typeof createUserSchema>;
export const userIdParamSchema = z.object({
id: z.uuid()
});
export const listUsersQuerySchema = z.object({
limit: z.coerce.number().int().min(1).max(100).default(10),
q: z.string().trim().min(1).optional()
});
z.coerce.number() полезен для значений из строк запроса, поскольку всё, что читается из HTTP-запроса, поступает в виде строки независимо от его логического типа. В таких случаях предпочтите safeParse вместо parse, чтобы иметь контроль над результатирующимся HTTP-статусом и телом ответа.
Единообразная форматировка ошибок
Преобразуйте ZodError.issues в одну стабильную JSON-структуру вместо отдельной обработки ошибок форматирования в каждом маршруте. Zod 4 также предоставляет функцию z.flattenError() для получения плоской карты ошибок с указанием полей, а функцию z.treeifyError() — для получения вложенной структуры, соответствующей схеме.
// format-zod-error.ts
import { ZodError } from 'zod';
export function formatZodError(error: ZodError) {
return {
message: 'Validation failed',
issues: error.issues.map((issue) => ({
path: issue.path.join('.') || '(root)',
message: issue.message,
code: issue.code
}))
};
}
Промежуточный компонент валидации
Проверьте значения body, query и params до запуска обработчика маршрута, затем сохраните преобразованные данные обратно, чтобы обработчик получал типизированные и приведенные к нужному виду данные.
// validate.ts
import { NextFunction, Request, Response } from 'express';
import { ZodType } from 'zod';
import { formatZodError } from './format-zod-error';
type RequestSchemas = {
body?: ZodType;
query?: ZodType;
params?: ZodType;
};
export function validate(schemas: RequestSchemas) {
return (req: Request, res: Response, next: NextFunction) => {
const parseOrReject = (schema: ZodType, value: unknown) => {
const parsed = schema.safeParse(value);
if (!parsed.success) {
res.status(400).json(formatZodError(parsed.error));
return null;
}
return parsed.data;
};
if (schemas.body) {
const body = parseOrReject(schemas.body, req.body);
if (body === null) return;
req.body = body;
}
if (schemas.query) {
const query = parseOrReject(schemas.query, req.query);
if (query === null) return;
res.locals.query = query;
}
if (schemas.params) {
const params = parseOrReject(schemas.params, req.params);
if (params === null) return;
res.locals.params = params;
}
next();
};
}
Подключите его к каждому маршруту следующим образом:
app.post('/users', validate({ body: createUserSchema }), (req, res) => {
// req.body is CreateUserInput
res.status(201).json({ id: crypto.randomUUID(), ...req.body });
});
app.get('/users', validate({ query: listUsersQuerySchema }), (req, res) => {
const { limit, q } = res.locals.query;
// ...
});
app.get('/users/:id', validate({ params: userIdParamSchema }), (req, res) => {
const { id } = res.locals.params;
// ...
});
Результаты обработки запроса и параметров хранятся в res.locals, поскольку типизация в Express рассматривает req.query/req.params как обычные карты строк; прямая замена их на другие структуры приведет к конфликту с этой типизацией.
Подводные камни
- Строки запросов всегда являются строками — для чисел и логических значений используйте
z.coerce(илиz.string()в сочетании с преобразованием). parseвыбрасывает необработанную ошибкуZodError; либо поймайте её и самостоятельно преобразуйте в ошибку 400, либо вместо этого используйтеsafeParse.- По умолчанию схемы объектов Zod удаляют неизвестные ключи; чтобы отклонять их, добавьте метод
.strict(). - Типы, определяемые автоматически, такие как
CreateUserInput, существуют только во время компиляции — всегда выполняйте парсинг и на границах данных. - В Zod 4 метод
z.uuid()проверяет данные согласно более новым и строгим спецификациям UUID; если вам нужен лишь общий шаблон из восьми, четырех, четырех и двенадцати шестнадцатеричных цифр без более строгих правил, используйте вместо этогоz.guid().
Альтернатива: проверка с использованием промежуточных компонентов express-validator
Zod — это не единственный способ не допускать некорректные данные в обработчики. Приложения Express уже давно используют express-validator — библиотеку, созданную специально в качестве промежуточного компонента для Express, которая применяет другой подход к решению той же проблемы.
Представьте запрос на регистрацию вот такого вида:
{
"email": "hello",
"password": "123"
}
Если контроллер проверяет этот пакет данных напрямую, для каждого поля требуется отдельная ручная проверка, что быстро превращается в множество условий, смешивающих проверку данных с бизнес-логикой:
if (!email) ...
if (!email.includes("@")) ...
if (!password) ...
if (password.length < 8) ...
express-validator перемещает эту логику из контроллера в отдельный шаг промежуточного компонента, так что запрос проходит через проверку до того, как дойдет до обработчика:
Request
↓
Validation
↓
Controller
↓
Business Logic
Именно в этом разделении суть библиотеки: контроллер остается свободным для выполнения только тех задач, для которых он предназначен.
Чтобы начать использование, установите пакет:
npm install express-validator
Импортируйте вспомогательный класс body и создайте цепочку проверок для каждого поля, которое вас интересует:
import { body } from "express-validator";
export const registerValidator = [
body("email")
.isEmail()
.withMessage("Invalid email"), body("password")
.isLength({ min: 8 })
.withMessage("Password must contain at least 8 characters"), body("username")
.notEmpty()
.withMessage("Username is required"),
];
Подключите этот промежуточный компонент к маршруту перед контроллером:
router.post(
"/register",
registerValidator,
registerController
);
Одних только определений проверок недостаточно — вам всё равно необходимо считывать ошибки, собранные в ходе верификации:
import { validationResult } from "express-validator";
const errors = validationResult(req);if (!errors.isEmpty()) {
return res.status(400).json({
errors: errors.array(),
});
}
Благодаря такой проверке недопустимые данные отклоняются с кодом 400 ещё до выполнения любой бизнес-логики.
Встроенные валидаторы хорошо справляются с распространёнными случаями:
.isEmail()
.isLength()
.notEmpty()
.isInt()
Но в реальных приложениях часто требуются правила, которые библиотека не может знать заранее — например, при регистрации возможно понадобится проверить, занят ли уже адрес электронной почты. Для этого и существует метод .custom():
body("email")
.isEmail()
.bail()
.custom(async (email) => {
const user = await User.findOne({ email });
if (user) {
throw new Error("Email already registered");
} return true;
});
Пользовательские валидаторы могут быть асинхронными, что делает их подходящими для поиска в базе данных и других проверок, зависящих от собственной логики приложения. Обратите внимание на вызов .bail() перед пользовательской проверкой — он пропускает остальную часть цепочки, включая асинхронный поиск, если адрес электронной почты уже не прошел проверку .isEmail(), тем самым избегая бесполезных запросов к базе данных.
Выбор между этими двумя библиотеками — или Joi, еще одним проверенным вариантом — зависит от того, что лучше подходит для вашей архитектуры: express-validator подходит для проектов, уже построенных на средствах Express middleware, Zod — для кодовых баз, ориентированных на TypeScript и схемы, а Joi представляет собой зрелую универсальную альтернативу. Универсально правильного выбора нет; он зависит от архитектуры вашего приложения.
Какой бы инструмент вы ни выбрали, сильные стороны express-validator — это встроенные валидаторы, инструменты для очистки данных, пользовательские и асинхронные валидаторы, модель мидлвэра и централизованная обработка ошибок. Чистый поток запросов в Express обычно выглядит следующим образом:
Request
↓
Validator
↓
Controller
↓
Service
↓
Database
Цель заключается не просто в подтверждении того, что строка похожа на адрес электронной почты — речь идет о максимально раннем отклонении некорректных данных, чтобы остальная часть приложения оставалась в порядке.
Связанные материалы
- Предложения TC39 в 2026 году: объяснение декораторов, Temporal и Signals — практический обзор трех предложений TC39: нативных декораторов, API Temporal и Signals, и того, что они значат для разработчиков full-stack JavaScript и TypeScript.