Als je een Node.js API bouwt, weet je dat je nooit zomaar kunt vertrouwen op wat er binnenkomt. Validatie met Zod maakt het valideren van inkomende data niet alleen veilig, maar ook type-safe en heerlijk beknopt. In plaats van handmatig elk veld te checken met if-statements, beschrijf je één keer hoe je data eruit moet zien en laat je Zod het zware werk doen.
In deze gids leer je hoe Zod werkt, hoe je schemas opbouwt, hoe je Zod integreert in je Express routes en welke valkuilen je moet vermijden. We bouwen voort op eerdere artikelen uit de serie over Node.js en HTTP servers en routing.
Wat is Zod en waarom gebruik je het?
Zod is een schema-declaratie en validatie library voor TypeScript en JavaScript. Je beschrijft de vorm van je data in een schema en Zod verzorgt twee dingen tegelijk. Ten eerste valideert het runtime-data tegen dat schema. Ten tweede leidt het automatisch TypeScript types af uit datzelfde schema.
Dat laatste is wat Zod onderscheidt van oudere libraries zoals Joi of Yup. Je hoeft geen aparte TypeScript interfaces te onderhouden naast je validatieregels. Eén bron van waarheid, geen drift tussen types en runtime checks.
Waarom is dat belangrijk? Omdat in een typische Node.js applicatie data binnenkomt via request bodies, query parameters, environment variables, externe API's en databases. Al die bronnen leveren in principe any of unknown terug. Zonder validatie accepteer je stilletjes corrupte data tot het ergens in je systeem ontploft.
Zod installeren en je eerste schema
Je installeert Zod met één commando in je project:
npm install zod
Daarna kun je meteen aan de slag. Een eenvoudig schema voor een gebruiker ziet er zo uit:
import { z } from "zod";
const UserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
type User = z.infer<typeof UserSchema>;
De z.infer helper haalt het TypeScript type uit je schema. In het voorbeeld hierboven krijgt User automatisch dit type:
type User = {
name: string;
email: string;
age?: number | undefined;
};
Verander je het schema, dan verandert het type mee. Geen dubbele administratie meer.
Data valideren: parse en safeParse
Zod biedt twee manieren om data te valideren. De eerste is parse, die een exception gooit als de data niet klopt:
const user = UserSchema.parse(req.body);
De tweede is safeParse, die een result object teruggeeft zonder te throwen:
const result = UserSchema.safeParse(req.body);
if (!result.success) {
console.log(result.error.issues);
return;
}
const user = result.data;
In productie-code is safeParse meestal de veilige keuze. Je bepaalt zelf wat er gebeurt bij een fout en voorkomt dat een onverwachte input je hele proces crasht.
Complexere schemas bouwen
Zod kan veel meer dan alleen strings en numbers. Je combineert primitives, objecten, arrays en unions tot rijke structuren. Hieronder zie je een schema voor een bestelling:
const OrderItemSchema = z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1),
price: z.number().nonnegative(),
});
const OrderSchema = z.object({
orderId: z.string().uuid(),
customer: z.object({
email: z.string().email(),
name: z.string(),
}),
items: z.array(OrderItemSchema).min(1),
status: z.enum(["pending", "paid", "shipped", "cancelled"]),
createdAt: z.coerce.date(),
});
Let op z.coerce.date(): dit converteert strings of nummers automatisch naar een Date object voordat het valideert. Handig wanneer je JSON payloads verwerkt waarin datums als ISO-strings binnenkomen.
Unions en discriminated unions
Soms kan een veld meerdere vormen aannemen. Voor nette foutmeldingen gebruik je discriminated unions:
const EventSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("click"), x: z.number(), y: z.number() }),
z.object({ type: z.literal("scroll"), position: z.number() }),
z.object({ type: z.literal("submit"), formId: z.string() }),
]);
Zod weet dan welk sub-schema het moet gebruiken op basis van het type veld, en geeft gerichtere errors.
Transformaties en refinements
Je kunt waarden tijdens het parsen transformeren of aanvullende checks uitvoeren:
const PasswordSchema = z
.string()
.min(8)
.refine((val) => /[A-Z]/.test(val), {
message: "Moet minstens één hoofdletter bevatten",
})
.refine((val) => /[0-9]/.test(val), {
message: "Moet minstens één cijfer bevatten",
});
const SlugSchema = z.string().transform((val) => val.toLowerCase().trim());
Met refine voeg je business rules toe, met transform normaliseer je data voor je applicatie erover nadenkt.
Zod integreren in een Express API
In een typische Express-applicatie wil je Zod gebruiken om request bodies, query parameters en URL params te valideren. Een herbruikbare middleware maakt dat netjes:
import type { Request, Response, NextFunction } from "express";
import type { ZodSchema } from "zod";
export const validate =
(schema: ZodSchema) =>
(req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: "Validation failed",
details: result.error.flatten(),
});
}
req.body = result.data;
next();
};
Gebruik daarna de middleware bij specifieke routes:
app.post("/users", validate(UserSchema), (req, res) => {
res.status(201).json({ user: req.body });
});
Het mooie: na de middleware is req.body gegarandeerd van het juiste type, inclusief defaults en transformaties.
Environment variables valideren
Een vaak vergeten toepassing: je .env variabelen valideren bij het opstarten van je applicatie. Dit voorkomt cryptische fouten wanneer een cruciale variabele ontbreekt in productie.
const EnvSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
});
export const env = EnvSchema.parse(process.env);
Als er iets ontbreekt, faalt je app direct bij het starten met een duidelijke melding. Dat is veel beter dan een undefined is not a function halverwege een request. Koppel dit aan wat je eerder leerde over modules en project structuur en je hebt een solide fundering.
Zod en databases
Wanneer je werkt met externe data uit databases of third-party API's, geldt dezelfde regel: vertrouw de data niet blind. Zelfs als je eerder een database hebt opgezet in Node.js, is het verstandig om de data die je terugkrijgt te valideren voordat je hem doorgeeft aan je applicatielaag. Schema-drift of handmatige migraties gebeuren vaker dan je denkt.
const ProductRowSchema = z.object({
id: z.string(),
name: z.string(),
price_cents: z.number().int(),
created_at: z.date(),
});
const rows = await db.query("SELECT * FROM products");
const products = z.array(ProductRowSchema).parse(rows);
Performance: wanneer moet je opletten?
Zod is snel genoeg voor de meeste toepassingen, maar er zijn een paar zaken om in de gaten te houden. Schemas definieer je eenmalig op module-niveau, niet binnen een request handler, want dan maak je het object bij elke request opnieuw. Bij gebruik van async patterns zoals promises en async/await let je erop dat validatie synchroon gebeurt; parseAsync bestaat wel voor async refinements, maar gebruik het alleen als je echt async checks nodig hebt.
Voor zeer grote arrays kun je overwegen om alleen steekproefsgewijs te valideren of validatie te beperken tot de randen van je systeem. Zie ook de officiële Zod documentatie voor geavanceerde patterns en de laatste features.
Best practices voor Zod in productie
Hanteer een paar simpele richtlijnen om je codebase schoon te houden:
- Definieer schemas op één plek per domein. Een
schemas/user.tsnaastmodels/user.tsvoorkomt dubbele waarheden. - Exporteer types via
z.infer. Gebruik ze overal in je applicatie in plaats van handgeschreven interfaces. - Gebruik
strict()voor onbekende velden. Dan faalt validatie wanneer de client extra velden stuurt, handig tegen typfouten. - Log validatie-errors gestructureerd. Gebruik
error.flatten()oferror.format()voor leesbare output. - Test je schemas. Schrijf unit tests voor edge cases; ze zijn de contracten van je API.
Voor meer inspiratie over veilige API design kun je de OWASP API Security Top 10 raadplegen. Validatie is een van de effectiefste maatregelen tegen injection en overtypering.
Veelgestelde vragen
Wat is Zod precies?
Zod is een TypeScript-first schema validatie library voor JavaScript en Node.js. Je beschrijft de vorm van je data met een schema en Zod valideert en parseert inkomende data volgens dat schema, met automatisch afgeleide TypeScript types.
Wat is het verschil tussen Zod en Joi?
Joi is een bekende validatie library, maar Zod is TypeScript-first en leidt automatisch types af uit je schemas. Dit betekent dat je geen aparte interfaces hoeft te onderhouden naast je validatieregels, wat dubbele code voorkomt.
Heb ik TypeScript nodig om Zod te gebruiken?
Nee, Zod werkt ook prima in gewone JavaScript projecten. De meerwaarde is wel het grootst in combinatie met TypeScript, omdat je dan automatisch type-safety krijgt zonder dubbele definities.
Is Zod snel genoeg voor productie?
Ja, Zod is ontworpen met performance in gedachten en wordt gebruikt in veel productie-applicaties. Voor hele grote payloads of kritieke hot paths kun je overwegen schemas te cachen of zwaardere checks te beperken.
Kan ik Zod gebruiken met Express of Fastify?
Zeker. Je wrapt je route handlers met een middleware die het request body, query of params parsed via een Zod schema. Bij een mislukte validatie stuur je een 400 response met duidelijke foutmeldingen terug.
Tot slot
Validatie met Zod tilt de betrouwbaarheid van je Node.js applicaties naar een hoger niveau, zonder dat je er veel code voor hoeft te schrijven. Één schema, runtime-veiligheid en type-safety in één klap. Begin klein door je belangrijkste API endpoints te valideren, breid daarna uit naar environment variables en externe data, en je zult merken hoe veel bugs je voorkomt voordat ze ooit in productie komen.