Complete "Lesson 4: Handling Request Data and Building CRUD Endpoints" first to unlock this lesson.Go to previous lesson

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:

bash
npm install express-validator

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:

javascript
const { body, validationResult } = require('express-validator');
const createUserRules = [
body('email').isEmail().withMessage('Must be a valid email address'),
body('password')
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters'),
];
router.post('/users', createUserRules, (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
// safe to use req.body here
res.status(201).json({ message: 'User created' });
});

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.

Flowchart showing request flow: incoming POST request -> validation middleware chain -> validationResult check -> if errors return 422 response, if no errors continue to route handler

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:

javascript
// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({
error: {
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
},
});
}
module.exports = errorHandler;

Register it after all your routes in the main app file. Order matters — Express only invokes this handler if it is defined last:

javascript
const errorHandler = require('./middleware/errorHandler');
app.use('/api', router);
app.use(errorHandler); // must come after all routes

Now any route can signal a problem by attaching a statusCode to an Error object and passing it to next:

javascript
router.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
const err = new Error('User not found');
err.statusCode = 404;
return next(err);
}
res.json(user);
} catch (error) {
next(error); // unexpected errors get 500
}
});

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:

javascript
// utils/AppError.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true; // distinguishes expected errors from bugs
}
}
module.exports = AppError;

The isOperational flag lets your error handler behave differently for errors you deliberately threw versus unexpected runtime crashes:

javascript
const AppError = require('../utils/AppError');
// in a route
if (!user) {
return next(new AppError('User not found', 404));
}

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.

Diagram showing two error paths: operational error (isOperational true) going to safe JSON response, and unexpected error (isOperational false) going to logging/alerting step then generic 500 response

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 practice

Lesson Checkpoint

Review what you learned and get feedback on your work.

0/1
Answered · 0/1