Галоўная / Артыкулы / 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() прычынам спецыяльнай пераконтроўкі — ён праходзіць праз рэшту ланцюга, уклjuчаючы асінхронны запыт, якщо адреса электранай павядомлення вяліка ўжо пераконтрацыю .isEmail(), тэму чаго ухілваецца ад непатрэбнай поўтарной пераадрасавання да базы дадзеных.

Выбір между гэтымі двумя бібліятекамі — або Joi, іншым усталеным варыянтом — залежыць ад таго, што падходзіць вашай структуре: express-validator падходзіць для проектаў, якія вже пабудаваны на абмежэннях Express, Zod падходзіць для кодавых баз, якія прыоритэтна выкарыстоўваюць TypeScript і базуюцца на схемах, а Joi ёсць зрэлым універсальным альтернатыўным варыянтом. Не існуе універсальна правильнага выбору; ён залежыць ад архітэктуры вашага прыемніка.

Незалежна адзінам з выбраных вас інструментаў, адзіныя супермоці express-validator — это яго вбудованыя верыфікаторы, адзінственнікі дадзеных, спецыяльныя та асінхронныя верыфікаторы, яго модэль мідлвэра і централізаваная обработка адзінакоў. Чыстая паеўздача запыткаў у Express зазвычай выглядае так:

Request
  ↓
Validator
  ↓
Controller
  ↓
Service
  ↓
Database

Галоўная мета — не проста паказаць, чы рошчына налягае на адрэс электроннай пашты, а як можно раней адхіліць некоректныя даны, каб рэшта прыкладнення застаўалася чыстай.

Спадневаная літэратура

  • RFC 9457: Адказанне пра стандартызаванні адпаведзяў на аблыканні HTTP API — Дазвольце вам дазнацца, як формат Problem Details з RFC 9457 стандартызуе адпаведзі ў разы аблыкання HTTP API, і як правільна ўжываць його ў прыемлі NestJS.
  • Адна схема Zod для React Frontend і Node Backend — Дазвольце вам дазнацца, як адна схема Zod можа пераверываць формы ў React, адпаведзі API, тэлы запытак у Express і зменныя сяродовішча, адночасна ствараючы адпаведныя типы ў TypeScript.
  • req-guard-lite: Мінімальны лімітэр выкарыстоўвання для Express на TypeScript — Дазнаецеся, як працюе лёгкі лімітэр выкарыстоўання для Express без жадных залежнасцей, ад стандартных настройкаў у памяці да масштабавання за дапамой Redis і стварэння спецыяльных ключоў.