Zod kontra express-validator: Dwie metody walidacji w Expressie
Porównuje walidację żądań opartą na schemacie z Zod z interfejsem middleware express-validator bazującym na łańcuchach, omawiając konfigurację, formatowanie błędów oraz częste pułapki.
Obsługa niezaufanych danych wejściowych to jeden z pierwszych problemów, które musi rozwiązać każda API Express, a istnieje więcej niż jeden sposób na to – od bibliotek opartych na schematach po bardziej proceduralne walidatory typu łańcuchowy. Ten artykuł omawia obie te metody, zaczynając od podejścia opartego na schematach, stworzonego przy użyciu Zod.
Walidacja żądań za pomocą Zod
Express sam w sobie nie przeprowadza żadnej walidacji przychodzących danych. Bez weryfikacji na poziomie granicy, obsługi ścieżek otrzymują surowe wartości req.body, req.query i req.params w takim stanie – pola liczbowe, które w rzeczywistości są ciągami znaków, pola całkowicie brakujące oraz dane, których struktura sprawia problemy dopiero w momencie dotarcia do logiki biznesowej.
Zod rozwiązuje ten problem, umożliwiając opisanie oczekiwanych struktur danych jako schematów typu TypeScript-first. Definiujesz schemat raz, uzyskujesz z niego typ statyczny za pomocą z.infer, a następnie analizujesz przychodzące dane na poziomie warstwy HTTP, dzięki czemu wszystkie elementy poniżej tego poziomu widzą wyłącznie poprawne dane. Wszystko, co nie przechodzi walidacji, może stać się odpowiedzią HTTP 400 jeszcze przed uruchomieniem kodu obsługującego żądanie.
Poniższe przykłady wykorzystują Zod 4 (z.email(), z.uuid(), z.coerce), a także middleware do walidacji w Express, pomocnik do formatowania błędów oraz listę najczęstszych pułapek.
Wymagania wstępne
Będziesz potrzebował wersji Node.js 26, Zod 4 (npm i zod) oraz Express wraz z jego definicjami typów (npm i express oraz npm i -D @types/express). Starsza składnia łańcuchowa Zod 3, taka jak z.string().email(), nadal działa w wersji 4, ale jest przestarzała – lepiej używać nowszych funkcji na najwyższym poziomie przedstawionych poniżej.
Deklarowanie schematów
// 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() jest przydatny dla wartości z ciągu zapytania, ponieważ wszystko, co zostanie odczytane z zapytania HTTP, trafia jako ciąg znaków, niezależnie od jego logicznego typu. Lepiej używać safeParse zamiast parse na granicy przetwarzania, aby mieć kontrolę nad statusem HTTP oraz treścią odpowiedzi.
Jednolite formatowanie błędów
Zmień ZodError.issues na jedną spójną strukturę JSON zamiast formatować błędy oddzielnie w każdej trasie. Zod 4 oferuje również z.flattenError() do uzyskania płaskiego mapu błędów z kluczami pól oraz z.treeifyError() do utworzenia zagnieżdżonej struktury odpowiadającej schematowi.
// 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
}))
};
}
Middleware walidacji
Waliduj body, query i params przed uruchomieniem obsługi trasy, a następnie zapisz przetworzone wartości z powrotem, aby obsługa otrzymała dane typowane i przekształcone.
// 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();
};
}
Podłącz to do każdej trasy w następujący sposób:
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;
// ...
});
Wyniki z query i params są przechowywane w res.locals, ponieważ typy Express traktują req.query/req.params jako zwykłe mapy ciągów znakowych; bezpośrednia ich zamiana wpłynęłaby negatywnie na tę typizację.
Potknięcia
- Łańcuchy zapytań są zawsze łańcuchami tekstowymi — użyj
z.coerce(lubz.string()w połączeniu z transformacją) dla liczb i wartości logicznych. parserzuca surowy błądZodError; albo sam go przechwycisz i przekształcisz na błąd 400, albo zamiast tego użyjsafeParse.- Schematy obiektów Zod domyślnie odrzucają nieznane klucze; dodaj
.strict(), aby je odrzucić. - Typy wywnioskowane, takie jak
CreateUserInput, istnieją tylko w czasie kompilacji — zawsze przetwarzaj dane również na granicy. - W Zod 4
z.uuid()sprawdza zgodność z nowszą, bardziej rygorystyczną specyfikacją UUID; jeśli potrzebujesz jedynie ogólnego wzoru ośmiu, czterech, czterech, czterech i dwunastu hexadecymalnych cyfr bez tych surowszych zasad, skorzystaj zamiast tego zz.guid().
Aльтernatywa: Walidacja oparta na middleware z express-validator
Zod nie jest jedynym sposobem na uniemożliwienie dostępu złych danych do funkcji obsługujących żądania. Aplikacje Express od dawna korzystają z express-validator, biblioteki stworzonej specjalnie jako middleware dla Express, która stosuje inne podejście do tego samego problemu.
Załóżmy prośbę o rejestrację wyglądającą w ten sposób:
{
"email": "hello",
"password": "123"
}
Jeśli kontroler bada ten payload bezpośrednio, każde pole wymaga osobistej ręcznej weryfikacji, co szybko przeradza się w mnóstwo warunków łączących walidację z logiką biznesową:
if (!email) ...
if (!email.includes("@")) ...
if (!password) ...
if (password.length < 8) ...
express-validator przenosi tę logikę poza kontroler do dedykowanego kroku middleware, dzięki czemu żądanie przechodzi przez proces walidacji zanim dotrze do funkcji obsługującej żądanie:
Request
↓
Validation
↓
Controller
↓
Business Logic
To rozdzielenie jest istotą tej biblioteki: kontroler może skupić się wyłącznie na tym, do czego został stworzony.
Aby rozpocząć, zainstaluj pakiet:
npm install express-validator
Importuj pomocnik body i utwórz łańcuch walidacji dla każdego pola, które Cię interesuje:
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"),
];
Przyłącz ten middleware do trasy, przed kontrolerem:
router.post(
"/register",
registerValidator,
registerController
);
Samo zdefiniowanie sprawdzeń nie wystarczy — nadal musisz odczytać wszystkie błędy zebrane podczas walidacji:
import { validationResult } from "express-validator";
const errors = validationResult(req);if (!errors.isEmpty()) {
return res.status(400).json({
errors: errors.array(),
});
}
Dzięki temu sprawdzeniu nieprawidłowe dane są odrzucane z kodem 400, zanim zostanie uruchomiona jakakolwiek logika biznesowa.
Budowane w bibliotece walidatory dobrze radzą sobie z typowymi przypadkami:
.isEmail()
.isLength()
.notEmpty()
.isInt()
Jednak w rzeczywistych aplikacjach często potrzebne są reguły, których biblioteka nie może przewidzieć z góry — na przykład podczas rejestracji możesz chcieć sprawdzić, czy adres e-mail jest już zajęty. Właśnie do tego służy .custom():
body("email")
.isEmail()
.bail()
.custom(async (email) => {
const user = await User.findOne({ email });
if (user) {
throw new Error("Email already registered");
} return true;
});
Własne walidatory mogą być asynchroniczne, co czyni je odpowiednimi do wyszukiwań w bazie danych oraz innych sprawdzeń zależnych od logiki specyficznej dla danego projektu. Zwróć uwagę na wywołanie .bail() przed własną weryfikacją — pomija ono resztę łańcucha, w tym asynchroniczną próbę wyszukiwania, jeśli adres e-mail już nie przeszedł sprawdzenia .isEmail(), co zapobiega bezcelowym operacjom w bazie danych.
Wybór pomiędzy tymi dwoma bibliotekami — a także Joi, inną sprawdzoną opcją — zależy od tego, co pasuje do Twojej architektury: express-validator nadaje się do projektów już zbudowanych wokół middleware Express, Zod jest odpowiedni dla kodów opartych na TypeScript i schematach, natomiast Joi stanowi dojrzałą alternatywę uniwersalną. Nie istnieje jedyny poprawny wybór; zależy to od architektury Twojej aplikacji.
Niezależnie od wybranej narzędzia, mocą express-validator są wbudowane walidatory, narzędzia do czyszczenia danych, dostosowalne i asynchroniczne walidatory, model middleware oraz centralizowane zarządzanie błędami. Czysty pipeline żądań w Express zazwyczaj wygląda w ten sposób:
Request
↓
Validator
↓
Controller
↓
Service
↓
Database
Celem nie jest tylko potwierdzenie, że dana strona tekstowa przypomina adres e-mail — chodzi o jak najszybsze odrzucenie nieprawidłowych danych, aby reszta aplikacji mogła funkcjonować bez problemów.
Literatura pokrewna
- Propozycje TC39 w 2026 roku: Dekoratory, Temporal i Signals wyjaśnione — praktyczny przegląd trzech propozycji TC39 – wbudowanych dekoratorów, API Temporal oraz Signals – i tego, co one oznaczają dla programistów JavaScript i TypeScript pracujących w pełnym stacku.