Главная / Статьи / Zod против express-validator: два подхода к валидации в Express

Zod против express-validator: два подхода к валидации в Express

Сравнивает проверку запросов с упором на схему с помощью Zod и промежуточный модуль express-validator, основанный на цепочках, рассматривая процесс настройки, форматирование ошибок и распространённые проблемы.

1438 слов

Обработка ненадежных данных входящего запроса — одна из первых проблем, с которыми должен столкнуться любой 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

Цель заключается не просто в подтверждении того, что строка похожа на адрес электронной почты — речь идет о максимально раннем отклонении некорректных данных, чтобы остальная часть приложения оставалась в порядке.

Связанные материалы

  • RFC 9457: Объяснение — стандартизация ответов на ошибки HTTP API — Узнайте, как формат описания проблем RFC 9457 стандартизирует ответы на ошибки HTTP API и как правильно реализовать его в приложении NestJS.
  • Использование одной схемы Zod в React-фронтенде и Node-бэкенде — Узнайте, как одна схема Zod может проверять формы в React, ответы API, тела запросов Express и переменные окружения, одновременно генерируя соответствующие типы TypeScript.
  • req-guard-lite: Минималистичный лимитер скорости работы для Express на TypeScript — Узнайте, как работает легкий лимитер скорости для Express без дополнительных зависимостей, от стандартных решений с использованием оперативной памяти до масштабирования с помощью Redis и пользовательских генераторов ключей.