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.

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.

bash
typescript

Zod Schema Patterns

typescript

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

Organising Your Schemas

Keep schemas next to the routes that use them:

text
typescript
typescript

The Async Wrapper: Eliminating try/catch Boilerplate

Every async route handler has the same try/catch wrapper. Remove it entirely:

typescript

The wrapper:

typescript

That's it. Any error thrown inside the async function — including AppError, ZodError, or PrismaClientKnownRequestError — is forwarded to next(err) automatically. The global error handler takes over from there.

With the wrapper, controllers become trivial to read and write:

typescript

No try/catch, no explicit next. Errors surface automatically.


The Complete Error Class Hierarchy

Extending the hierarchy from P-1 to cover every scenario:

typescript

Services throw these; the error handler maps them to HTTP responses. No HTTP status codes anywhere in the business logic layer.


The Global Error Handler

One function handles every possible error type:

typescript

Register it after all routes in index.ts:

typescript

The four-parameter signature is how Express recognises an error handler. If you accidentally use three parameters, it will be treated as regular middleware and errors will pass through it silently.


Putting It All Together: A Complete Route

Here is a full POST /orders route with validation, auth, and async error handling — no boilerplate:

typescript
typescript
typescript

The error flows: service throws NotFoundError → asyncHandler catches it → passes to next(err) → error handler maps it to 404 { error: 'Order not found' }. The controller never needs to know.


Consistent Error Response Shape

Define the shape once in your API documentation and stick to it:

json

Every error, every route, same shape. Frontend developers write one error handler, not one per endpoint.


Request ID Middleware

Add a request ID to every request for correlating logs with errors:

typescript
typescript

Include the request ID in error logs:

typescript

Now when a client reports an error, they provide the x-request-id response header and you can find exactly that request in your logs.


Summary

  • Zod validates input at runtime and infers TypeScript types — define the schema once, get both validation and types.
  • validate(schema, target) middleware runs before controllers and returns 400 with structured error messages if input is invalid. Replace req[target] with the parsed data so downstream code gets transformed, coerced values.
  • asyncHandler wraps async route handlers and forwards thrown errors to next(err). Eliminates try/catch boilerplate from every controller.
  • Typed error classes (NotFoundError, ConflictError, etc.) let services express failure without knowing about HTTP. The error handler translates them to status codes.
  • The global error handler handles Zod errors, AppErrors, Prisma errors, JWT errors, and unknown errors in one place. Register it last, with four parameters.
  • Consistent error shape{ error: string, issues?: [] } for every failure — lets the frontend handle errors generically.

Next: testing — unit testing services with Jest, integration testing routes with Supertest, and mocking the layers below the unit under test.


Knowledge Check

How does Express recognize a middleware function as a global error handler?


What is the primary purpose of an asyncHandler utility in an Express application?


When using Zod in validation middleware, why is it beneficial to replace the request object data (e.g., req.body = parsedData) after successful validation?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

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