Kodokon kodokon.com

Input validation and error middleware

Validate data at the edge of the API and centralize all error handling in a single four-parameter middleware.

9 min · 3 questions

Open this lesson in Kodokon

Every incoming piece of data is hostile until proven otherwise: JSON body, URL parameters, headers. Validation belongs at the edge of the application - before the controller - so that inner layers work on guaranteed data. Then distinguish two families of errors: operational errors (invalid input, missing resource, conflict), expected and translated into 4xx, and programming errors (bugs), which must produce an opaque 500 on the client side and a full stack trace on the server side.

JAVASCRIPT
export class HttpError extends Error {
  constructor(status, message, details = undefined) {
    super(message);
    this.status = status;
    this.details = details;
  }
}
An error that carries its HTTP status: the basic building block.

A validation middleware intercepts the request before the controller. The manual version below shows the principle; in production, a declarative schema (zod has become the standard) additionally provides type inference and structured messages. The trade-off: one dependency and a slight runtime cost, in exchange for centralized, composable rules that are impossible to forget.

JAVASCRIPT
import { HttpError } from './http-error.js';

export function validateUser(req, res, next) {
  const { email, name } = req.body ?? {};
  const errors = [];
  if (typeof email !== 'string' || !email.includes('@')) {
    errors.push('email: invalid format');
  }
  if (typeof name !== 'string' || name.trim().length < 2) {
    errors.push('name: 2 characters minimum');
  }
  if (errors.length > 0) {
    return next(new HttpError(400, 'Invalid body', errors));
  }
  next();
}
Wire it up with router.post('/', validateUser, controller.create).

Express recognizes an error middleware by its four-parameter signature (err, req, res, next). Registered last, after all routes with app.use(errorHandler), it becomes the API's single error exit: a homogeneous response format, logging in one place, no leaking of internal information. The controllers, for their part, simply call next(err).

JAVASCRIPT
export function errorHandler(err, req, res, next) {
  if (res.headersSent) {
    return next(err);
  }
  const status = err.status ?? 500;
  const body = { error: err.message };
  if (err.details) {
    body.details = err.details;
  }
  if (status >= 500) {
    console.error(err);
    body.error = 'Internal server error';
  }
  res.status(status).json(body);
}
4xx transparent to the client, 500 masked but logged.

Knowledge check

Make sure you remember the key points of this lesson.

  1. How does Express tell an error middleware apart from an ordinary one?
    • By its name, which must contain the word error
    • By its four-parameter signature (err, req, res, next)
    • By its position first in the chain
    • By an explicit call to app.setErrorHandler
  2. What is the right HTTP response to an unexpected programming error?
    • A 500 with the message and stack to help diagnosis
    • A 400 to signal that the request failed
    • A 500 with a generic message, the full error being logged on the server side
  3. In Express 4, what happens if an async handler rejects without try/catch?
    • The error still reaches the error middleware
    • The request stays hung: the rejection is never forwarded to next
    • Express automatically returns a 400
    • The Node process stops immediately