Express.js middleware patterns for production APIs

February 2026

Express.js is often described as minimal, but most real-world Express applications rely heavily on middleware. Middleware defines how requests enter the system, how they are validated, and how failures are handled.

In production APIs, stability and clarity come less from individual routes and more from a well-structured middleware pipeline.

Middleware as a request pipeline

Middleware in Express works as a sequential pipeline. Each function receives the request, decides whether to continue, and optionally modifies the request or response.

// Order matters: each middleware builds on the previous one
app.use(requestLogger);
app.use(apiVersioning);
app.use(rateLimiting);
app.use(routes);
app.use(globalErrorHandler);

A predictable pipeline reduces hidden coupling and makes debugging significantly easier as the application grows.

API versioning using middleware

API versioning is a cross-cutting concern. Implementing version checks inside individual routes leads to duplication and inconsistent behavior. Middleware provides a centralized and explicit solution.

// Enforces API versioning based on URL prefix
const requireApiVersion = (version) => {
  return (req, res, next) => {
    if (req.path.startsWith(`/api/${version}`)) {
      return next();
    }

    res.status(404).json({
      success: false,
      error: "API version not supported",
    });
  };
};

This approach allows multiple API versions to coexist while keeping routing logic clean and maintainable.

Writing focused custom middleware

Custom middleware should have a single responsibility. Logging, validation, and authorization are easier to reason about when they are separated into small, focused functions.

// Logs incoming requests with timestamp and request metadata
const requestLogger = (req, res, next) => {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] ${req.method} ${req.originalUrl}`);
  next();
};

Lightweight logging middleware becomes especially useful when investigating production issues or analyzing traffic patterns.

Consistent error handling

Error handling is one of the most critical middleware responsibilities. Without a clear strategy, asynchronous errors can result in inconsistent responses or unhandled promise rejections.

// Custom error type for known application errors
class APIError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
  }
}

// Wraps async route handlers and forwards errors to Express
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

Centralizing error handling ensures that all failures are processed consistently and that internal details are not exposed to clients.

// Global error-handling middleware
const globalErrorHandler = (err, req, res, next) => {
  if (err instanceof APIError) {
    return res.status(err.statusCode).json({
      status: "error",
      message: err.message,
    });
  }

  res.status(500).json({
    status: "error",
    message: "Internal server error",
  });
};

Rate limiting for operational stability

Rate limiting helps protect APIs from excessive traffic and reduces the risk of unintentional overload. It also provides predictable behavior under high request volumes.

import rateLimit from "express-rate-limit";

// Factory function for reusable rate limit configurations
const createRateLimiter = ({ max, windowMs }) =>
  rateLimit({
    max,
    windowMs,
    message: "Too many requests, please try again later",
    standardHeaders: true,
    legacyHeaders: false,
  });

Applying rate limits selectively allows different endpoints to have different usage characteristics without affecting the entire system.

Explicit CORS configuration

CORS issues are easier to manage when configuration is explicit. Clearly defining allowed origins, headers, and methods avoids unexpected access patterns.

import cors from "cors";

const allowedOrigins = ["http://localhost:3000"];

const configureCors = () =>
  cors({
    origin(origin, callback) {
      if (!origin || allowedOrigins.includes(origin)) {
        callback(null, true);
      } else {
        callback(new Error("Origin not allowed by CORS"));
      }
    },
    methods: ["GET", "POST", "PUT", "DELETE"],
    allowedHeaders: ["Content-Type", "Authorization", "Accept-Version"],
    credentials: true,
  });

Middleware defines system behavior

Middleware is not just infrastructure code. It defines how an API behaves under normal conditions, how it responds to invalid input, and how it handles unexpected situations.

Well-designed middleware keeps Express applications predictable, maintainable, and resilient as they evolve.