Introduction
Every API receives data it did not create. Users send malformed JSON, missing fields, strings where numbers belong, and payloads that are technically valid but logically wrong. Without a validation layer, those problems quietly corrupt your database or trigger cryptic 500 errors. Without a consistent error-handling layer, every route does something different when things go wrong — some return plain strings, some return HTML error pages, some return nothing at all.
This lesson builds both layers: request validation using the express-validator library, and a centralized error handler that gives every error response the same predictable shape.
Validating Incoming Request Data
express-validator wraps the well-tested validator.js library and plugs directly into Express middleware. Install it alongside your project:
Validation rules are defined as an array of middleware and attached to a route before the handler runs. Here is a POST /users route that requires a valid email and a password of at least eight characters:
Calling validationResult(req) collects every violation that the rule chain found. If the array is not empty, the request is rejected immediately with HTTP 422 Unprocessable Entity — the semantically correct status when the server understands the request but cannot act on it because the data is invalid.
You can extract the validation rules into a separate file as your route count grows, keeping route files focused only on business logic.
Building a Centralized Error Handler
Express has a special middleware signature for error handling: four parameters instead of three. Any middleware or route that calls next(err) or throws inside an async wrapper will skip regular middleware and land here.
Create a dedicated file for it:
Register it after all your routes in the main app file. Order matters — Express only invokes this handler if it is defined last:
Now any route can signal a problem by attaching a statusCode to an Error object and passing it to next:
Every error, expected or not, now flows through the same handler and returns the same JSON shape. Frontend teams can write a single error-parsing function instead of guessing what each endpoint returns.
Creating a Custom AppError Class
Passing raw Error objects with manually attached statusCode properties gets repetitive. A small custom class cleans this up:
The isOperational flag lets your error handler behave differently for errors you deliberately threw versus unexpected runtime crashes:
You can check isOperational in the error handler to decide whether to log a full stack trace, send an alert, or simply return the message as-is.
Common Mistakes
Putting the error handler before your routes is the most frequent mistake — it never triggers because no error has been passed yet. Always register it last.
Using HTTP 400 for every client error is imprecise. Use 400 for malformed syntax, 401 when authentication is missing, 403 when the user is authenticated but not allowed, 404 when a resource does not exist, and 422 when the structure is valid but the content fails business rules.
Leaking stack traces in production is a real security risk. The conditional spread in the errorHandler example above (process.env.NODE_ENV === 'development') ensures stacks only appear locally.
Forgetting to wrap async route handlers in try/catch means unhandled promise rejections bypass your error handler entirely. A thin async wrapper utility can enforce this pattern across all routes without boilerplate.
Summary
Validation with express-validator stops bad data at the door and returns structured feedback to the client. A centralized error handler with a consistent JSON shape makes your API predictable for consumers and easier to maintain as it grows. The AppError class bridges both concerns — giving you a clean way to signal expected failures from anywhere in your codebase and letting one handler decide how to respond.
Lesson Checkpoint
Review what you learned and get feedback on your work.