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 — та їхнього значення для розробників JavaScript та TypeScript фул-стеку.