Zod gegen Express-Validator: Zwei Ansätze für die Validierung in Express
Vergleicht die Validierung von Anfragen nach dem Schema-Prinzip mit Zod im Vergleich zum auf Ketten basierenden express-validator-Middleware, wobei Setup, Fehlerformatierung und häufige Fallstricke behandelt werden.
Die Handhabung unzuverlässiger Eingaben ist eines der ersten Probleme, die jede Express-API lösen muss, und es gibt mehr als einen Weg, dies zu tun – von libraries, die auf Schemata basieren, bis hin zu prozeduraleren, kettenbasierten Validatoren. Dieser Artikel betrachtet beide Ansätze, beginnend mit einem auf Schemata ausgerichteten Verfahren, das auf Zod aufbaut.
Anfragen mit Zod validieren
Express führt keine eigene Validierung der eingehenden Daten durch. Ohne Überprüfung an der Grenze erhalten die Route-Handler das rohe req.body, req.query und req.params in ihrem ursprünglichen Zustand – numerische Felder, die eigentlich Strings sind, Felder, die völlig fehlen, sowie Payloads, deren Struktur erst Probleme verursacht, wenn sie Ihre Geschäftslogik erreichen.
Zod löst dieses Problem, indem es Ihnen ermöglicht, erwartete Datenschemata als TypeScript-basierte Schemata zu beschreiben. Sie definieren ein Schema einmal, leiten daraus mit z.infer einen statischen Typ ab und parsen die eingehenden Daten an der Grenze Ihrer HTTP-Schicht, sodass alle nachfolgenden Komponenten nur gültige Daten erhalten. Alles, was die Validierung nicht besteht, kann bereits vor dem Ausführen Ihres Handler-Codes zu einer HTTP 400-Antwort werden.
Die untenstehenden Beispiele verwenden Zod 4 (z.email(), z.uuid(), z.coerce), außerdem ein Express-Validierungs-Middleware, einen Hilfsfunktion zur gemeinsamen Fehlerformatierung sowie eine Liste von häufigen Problemen.
Voraussetzungen
Es werden Node.js Version 26, Zod 4 (npm i zod) sowie Express mit seinen Typdefinitionen (npm i express und npm i -D @types/express) benötigt. Die ältere verschachtelte Syntax von Zod 3, wie z. B. z.string().email(), funktioniert zwar auch in Version 4 noch, ist aber veraltet – verwenden Sie lieber die neueren Funktionen auf oberster Ebene, wie weiter unten gezeigt.
Schemata deklarieren
// 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() ist nützlich für Werte in Abfragesätzen, da alles, was aus einem HTTP-Abfragesatz gelesen wird, unabhängig von seinem logischen Typ als String ankommt. Verwenden Sie an der Grenze lieber safeParse statt parse, um die Kontrolle über den resultierenden HTTP-Status und den Antwortkörper zu behalten.
Fehler konsistent formatieren
Übersetzen Sie ZodError.issues in eine einheitliche, stabile JSON-Struktur um, anstatt Formatierungsfehler in jeder Route separat darzustellen. Zod 4 bietet außerdem z.flattenError() für ein flaches, nach Feldschlüsseln geordnetes Fehlerverzeichnis sowie z.treeifyError() für eine verschachtelte Struktur, die dem Schema entspricht.
// 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
}))
};
}
Validierungs-Middleware
Validieren Sie body, query und params vor dem Ausführen des Route-Handlers, schreiben Sie die analysierten Werte anschließend wieder zurück, damit der Handler typisierte, umgewandelte Daten erhält.
// 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();
};
}
Verbinden Sie es wie folgt pro Route:
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;
// ...
});
Die Ergebnisse von Query und params werden in res.locals gespeichert, da Express’ Typisierung req.query/req.params als einfache String-Maps behandelt; ein direkter Ersatz würde mit dieser Typisierung kollidieren.
Ausnahmesituationen
- Abfragesätze sind immer Zeichenketten – verwenden Sie
z.coerce(oderz.string()in Kombination mit einer Transformation) für Zahlen und Boolesche Werte. parsewirft einen rohenZodErroraus; fangen Sie diesen ab und wandeln Sie ihn selbst in einen 400-Fehler um, oder verwenden Sie stattdessensafeParse.- Schemata von Zod-Objekten entfernen standardmäßig unbekannte Schlüssel; fügen Sie
.strict()hinzu, um diese abzulehnen. - Ausgeleitete Typen wie
CreateUserInputexistieren nur zur Kompilierzeit – führen Sie auch immer die Analyse an der Grenze durch. - In Zod 4 prüft
z.uuid()nach der neueren, strengeren UUID-Spezifikation; wenn Sie lediglich ein generisches Muster aus acht, vier, vier, vier und zwölf Hexadezimalziffern ohne die strengeren Regeln benötigen, verwenden Sie stattdessenz.guid().
Eine Alternative: Validierung mittels Middleware mit express-validator
Zod ist nicht die einzige Möglichkeit, um schlechte Eingaben von Ihren Handlern fernzuhalten. Express-Anwendungen verlassen sich seit Langem auf express-validator, eine Bibliothek, die speziell als Express-Middleware entwickelt wurde, und sie verfolgt einen anderen Ansatz für dasselbe Problem.
Stellen Sie sich eine Registrierungsanfrage wie diese vor:
{
"email": "hello",
"password": "123"
}
Falls ein Controller dieses Payloads direkt überprüft, benötigt jedes Feld eine eigene manuelle Überprüfung, was schnell zu einer Flut an bedingten Anweisungen führt, die Validierung mit Geschäftslogik vermischen:
if (!email) ...
if (!email.includes("@")) ...
if (!password) ...
if (password.length < 8) ...
express-validator verlagert diese Logik aus dem Controller in einen speziellen Middleware-Schritt, sodass die Anfrage vor Erreichen Ihres Handlers durch die Validierung geleitet wird:
Request
↓
Validation
↓
Controller
↓
Business Logic
Diese Trennung ist der ganze Sinn der Bibliothek: Ihr Controller kann sich darauf konzentrieren, nur das zu tun, wofür er bestimmt ist.
Um anzufangen, installieren Sie das Paket:
npm install express-validator
Importieren Sie den body-Helper und erstellen Sie eine Validierungskette für jedes Feld, das für Sie wichtig ist:
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"),
];
Fügen Sie dieses Middleware-Element der Route vor dem Controller hinzu:
router.post(
"/register",
registerValidator,
registerController
);
Die Definition der Überprüfungen allein reicht nicht aus – Sie müssen weiterhin die während der Validierung gesammelten Fehler abrufen:
import { validationResult } from "express-validator";
const errors = validationResult(req);if (!errors.isEmpty()) {
return res.status(400).json({
errors: errors.array(),
});
}
Durch diese Überprüfung werden ungültige Datenpakete bereits vor dem Ausführen jeglicher Geschäftslogik mit einem 400-Fehler abgelehnt.
Die eingebauten Validatoren decken die gängigen Fälle gut ab:
.isEmail()
.isLength()
.notEmpty()
.isInt()
Aber echte Anwendungen benötigen oft Regeln, die die Bibliothek im Voraus nicht kennen kann – beispielsweise müssen Sie bei der Registrierung prüfen, ob eine E-Mail bereits in Gebrauch ist. Dafür dient .custom():
body("email")
.isEmail()
.bail()
.custom(async (email) => {
const user = await User.findOne({ email });
if (user) {
throw new Error("Email already registered");
} return true;
});
Customisierte Validatoren können asynchron sein, was sie für Datenbankabfragen und andere Überprüfungen geeignet macht, die von der eigenen Domänenlogik abhängen. Beachten Sie den Aufruf von .bail() vor der benutzerdefinierten Überprüfung – er überspringt den Rest der Kette, einschließlich der asynchronen Abfrage, wenn die E-Mail bereits bei der .isEmail()-Überprüfung fehlgeschlagen ist, wodurch eine sinnlose Datenbankanfrage vermieden wird.
Die Wahl zwischen diesen beiden Bibliotheken – oder Joi, einer weiteren etablierten Option – hängt davon ab, was zu Ihrer Technologiestack passt: express-validator eignet sich für Projekte, die bereits um Express-Middleware aufgebaut sind, Zod für TypeScript-basierte, schema-gesteuerte Codebasen, und Joi ist eine ausgereifte Allzweckalternative. Es gibt keine universell richtige Wahl – es hängt von der Architektur Ihrer Anwendung ab.
Egal, welches Werkzeug Sie wählen – die Stärken von express-validator liegen in seinen integrierten Validatoren, Sanitisierungs-Hilfsmitteln, benutzerdefinierten sowie asynchronen Validatoren, seinem Middleware-Modell und der zentralisierten Fehlerbehandlung. Ein sauberer Express-Anfragen-Pipeline sieht im Allgemeinen so aus:
Request
↓
Validator
↓
Controller
↓
Service
↓
Database
Es geht niemals nur darum, zu überprüfen, ob eine Zeichenkette wie eine E-Mail-Adresse aussieht – vielmehr sollen fehlerhafte Eingaben so früh wie möglich abgelehnt werden, damit der Rest der Anwendung sauber bleibt.
Zusätzliche Literatur
- TC39-Vorschläge im Jahr 2026: Erklärung zu Decorators, Temporal und Signals – Ein praktischer Überblick über drei TC39-Vorschläge – native Decorators, die Temporal-API und Signals – sowie deren Bedeutung für Full-Stack-JavaScript- und TypeScript-Entwickler.