DEV Community

Young Gao
Young Gao

Posted on

Express Middleware Patterns: Composition, Error Handling, and Auth (2026 Guide)

Your Express app has 40 middleware functions. Some run on every route. Some run on three. Nobody knows what order they execute. Time to fix that.

The Middleware Pipeline

Middleware executes in the order you register it. Think of it as a pipeline: request enters from the left, passes through each function, and the response flows back.

Request -> Logger -> Auth -> Validate -> Handler -> Error Handler -> Response
Enter fullscreen mode Exit fullscreen mode

Request Timing Middleware

function requestTimer(): RequestHandler {
  return (req, res, next) => {
    const start = process.hrtime.bigint();
    res.on("finish", () => {
      const ms = Number(process.hrtime.bigint() - start) / 1e6;
      logger.info({ method: req.method, path: req.path, status: res.statusCode, ms });
    });
    next();
  };
}
Enter fullscreen mode Exit fullscreen mode

Auth Middleware with JWT

function authenticate(): RequestHandler {
  return (req, res, next) => {
    const token = req.headers.authorization?.split(" ")[1];
    if (\!token) return res.status(401).json({ error: "Missing token" });
    try { req.user = jwt.verify(token, process.env.JWT_SECRET\!); next(); }
    catch { res.status(401).json({ error: "Invalid token" }); }
  };
}
Enter fullscreen mode Exit fullscreen mode

Error Handling Middleware

Express error handlers have 4 parameters. They MUST be registered last:

class AppError extends Error {
  constructor(public statusCode: number, message: string) { super(message); }
}
function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
  const status = err instanceof AppError ? err.statusCode : 500;
  logger.error({ err, method: req.method, path: req.path });
  res.status(status).json({
    error: status === 500 ? "Internal server error" : err.message,
  });
}
Enter fullscreen mode Exit fullscreen mode

Composing Middleware

Group related middleware into route-specific stacks:

const publicRoutes = [requestTimer(), cors()];
const authRoutes = [...publicRoutes, authenticate(), rateLimiter()];
const adminRoutes = [...authRoutes, requireRole("admin")];

app.get("/api/public", ...publicRoutes, publicHandler);
app.post("/api/orders", ...authRoutes, createOrder);
app.delete("/api/users/:id", ...adminRoutes, deleteUser);
Enter fullscreen mode Exit fullscreen mode

Common Mistakes

  1. Forgetting next(): Request hangs forever with no response
  2. Error handler not last: Express only calls the 4-parameter handler if it is registered after all routes
  3. Not catching async errors: Express 4 does not catch rejected promises. Wrap async handlers or use express-async-errors.
  4. Leaking internal errors to clients: Never send stack traces or DB errors in production responses

Part of my Production Backend Patterns series. Follow for more practical backend engineering.


If this was useful, consider:

Top comments (0)