Module P-4·22 min read

Zod for runtime schema validation, custom error classes, Express global error handler, the async wrapper that eliminates try/catch in every route handler.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module P-4 — Input Validation, Error Handling, and Middleware Pipelines

What this module covers: Every route handler needs two things before it touches business logic: confirmation that the input is valid, and a plan for when something goes wrong. This module covers Zod for runtime schema validation, the async wrapper pattern that eliminates try/catch boilerplate from every handler, the custom error class hierarchy from P-1 extended with validation errors, and building the global error handler that translates every possible failure into a clean JSON response. By the end your handlers will be ten lines each and your error responses will be consistent across every route.


The Problem with Manual Validation

Without a validation layer, every handler contains the same repetitive guard code:

javascript

This is brittle (the email check is wrong), not reusable, and not consistent. The same logic gets copy-pasted and diverges. A schema library solves all of this.


Zod: Runtime Type Safety

Zod lets you define a schema once and get three things for free: validation, error messages, and TypeScript types.

TypeScript is the visa you're issued before you leave home; Zod is the customs officer physically checking your passport at the border — the visa can say whatever it wants, but nothing crosses without the officer's runtime check. TypeScript's guarantees evaporate the moment tsc finishes compiling; Zod's parse/safeParse calls run against every real request, on every deploy, for as long as the service is up.

bash
typescript

Zod Schema Patterns

typescript

Transforms and Validating Headers

.transform() lets a schema change the shape of the data, not just check it — and z.infer follows the output type through the transform automatically:

typescript

This is easy to miss when refactoring: z.infer always reflects what the schema produces after transforms run, not what it accepts. If a teammate adds a .transform() to an existing schema, every consumer's inferred type changes with it — TypeScript will flag the mismatches, but only if you re-run tsc after the change.

Headers are just as validatable as body/query/paramsreq.headers is a plain object, so the same validate() middleware works once you widen its target type:

typescript
typescript

Two gotchas specific to headers: Node lower-cases incoming header names, so schema keys must be lowercase ('x-api-key', never 'X-API-Key'); and reassigning req.headers = result.data — the same pattern validate() uses for body/query/params — replaces the entire headers object, so make sure your schema doesn't accidentally drop headers other middleware still needs.


The Validate Middleware

Wrap Zod parsing into a reusable Express middleware factory:

typescript

Usage — schemas run before the controller, controller gets clean validated data:

typescript

When validation fails, the middleware returns before the controller is called:

json

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.