Middleware and next()

Express · Practical · 13 min

Middleware is Express's core idea: logging, parsing, auth, CORS and error handling are all middleware.

A middleware is a function (req, res, next) that runs during a request. It can do any of three things:

  • Respond and end the cycle (res.status(401).json(...)).
  • Modify req/res and call next() to pass control to the next middleware or route.
  • Fail by calling next(err) (or, in Express 5, throwing or rejecting), which skips straight to error-handling middleware.

If it does none of these, the request hangs.

app.use(fn) runs fn for every request; app.use("/admin", fn) only for paths starting with /admin (a prefix match: /admin, /admin/users, not /administrator). Inside, req.url is relative to the mount path while req.originalUrl keeps the full URL. Route handlers are middleware too — app.get(path, a, b, c) is a chain of three.

Order is everything. Express runs matching middleware and routes in exactly the order you registered them. Typical order:

  • Request-wide setup: request ID, logging, security headers, CORS, body parsers.
  • Authentication (populating req.user).
  • Routes and routers.
  • A 404 handler for anything unmatched.
  • Error handlers (four arguments) last.

A logger registered after your routes never sees requests that routes answered; a body parser registered after a route leaves req.body undefined in that route.

To run code after the response is sent — timing, metrics, logs — don't wait in the middleware; listen for res.on("finish"). Because async middleware is supported in Express 5, you can await inside and simply call next() when done; a rejection is forwarded as an error.

Use res.locals to pass per-request data to later middleware and templates without polluting req.

Example

import express from "express";
const app = express();

// 1. Timing: register first so it sees every request
app.use((req, res, next) => {
  const start = performance.now();
  res.on("finish", () => {
    console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${(performance.now() - start).toFixed(1)}ms`);
  });
  next();
});

app.use(express.json());

// 2. Path-scoped async middleware
app.use("/admin", async (req, res, next) => {
  const user = await lookupUser(req.get("authorization"));   // a rejection -> error handler
  if (!user?.isAdmin) return res.status(403).json({ error: "admins only" });
  res.locals.user = user;
  next();
});

app.get("/admin/stats", (req, res) => res.json({ viewer: res.locals.user.name }));
app.get("/", (req, res) => res.send("home"));

async function lookupUser(header) { return header === "Bearer admin" ? { name: "ada", isAdmin: true } : null; }
app.listen(3000);

Remember

  • Middleware either responds, calls next(), or passes an error
  • app.use(path, fn) is a prefix match; registration order is execution order
  • Use res.on('finish') for after-response work and res.locals for per-request data

Watch out for

  • Registering body parsers or auth after the routes that need them.
  • Forgetting to call next() in a non-responding middleware, hanging the request.

Check yourself

  1. A middleware logs and doesn't call next() or send a response. What does the client experience?
  2. app.use('/api', fn) is registered. For which request does fn run?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: expressjs.com/en/guide/using-middleware