← Node & Express

Middleware

What is a middleware in Express and how does it work within a request's lifecycle?

Show answer — try answering out loud first

A middleware is a function that receives three arguments: (req, res, next). It runs inside the pipeline (the chain of functions) that Express runs for every request. A middleware can read and modify the request object (req) and the response object (res), finish the response (for example with res.send(...)), or call next() to pass control to the next middleware. It's registered with app.use(...).

When an HTTP request arrives at your Express server, that request doesn't jump straight to your final route. It passes through a queue of functions, one after another, like a conveyor belt. Each of those functions is a middleware.

Every middleware always receives three things:

  • req: the request object. This is where everything the client sent lives: the URL, the headers, the body, the query params. You can read it and also add information to it (for example req.usuario = ...).
  • res: the response object. You use it to reply to the client (res.send, res.json, res.status).
  • next: a function that, when called, tells Express: "I'm done with my part, pass the request to the next one in the queue".

With those tools, a middleware has only two valid paths:

  1. Finish the response itself (by calling res.send, res.json, res.end, etc.). At that point the request ends and doesn't pass to anyone else.
  2. Call next() to hand control to the next middleware or to the final route.

The key point: it must do one of the two things. If it doesn't finish the response and doesn't call next() either, the request stays hung and the client waits forever.

Middleware is useful for tasks that repeat across many routes and that you don't want to copy and paste: logging, authenticating users, parsing the body (as express.json() does), validating data, measuring times, adding security headers, etc.

They're registered with app.use(...). If you give it a path (app.use("/api", ...)) it only runs for that path; if you don't give it a path, it runs for all requests.

const express = require("express");
const app = express();

// A simple logging middleware.
// It runs on EVERY request because we don't pass it a path.
function logger(req, res, next) {
  // We read request information from req.
  const inicio = Date.now();
  console.log(`Entró: ${req.method} ${req.url}`);

  // We hand control to the next middleware or route.
  // If we forget this next(), the request stays hung.
  next();

  // (In a real case, to measure the response time
  // you'd use res.on("finish", ...), not a line after next()).
  console.log(`Salió: tardó ${Date.now() - inicio}ms`);
}

// We register the middleware. ORDER matters: it must go BEFORE the routes.
app.use(logger);

// Middleware that adds data to req for the routes to use.
app.use((req, res, next) => {
  req.horaDeLlegada = new Date().toISOString();
  next();
});

// The final route: here we DO finish the response.
app.get("/", (req, res) => {
  res.json({ mensaje: "Hola", llegada: req.horaDeLlegada });
});

app.listen(3000, () => {
  console.log("Servidor en http://localhost:3000");
});

Forgetting to call next() (and not responding either) inside the middleware. When that happens, the request never advances: it doesn't reach the route, it doesn't respond with anything, and the client keeps waiting until the browser or client times out. The golden rule is: every middleware must either finish the response or call next(); never both, and never neither. Another related mistake is calling next() and also responding, which usually causes the error Cannot set headers after they are sent to the client.

"A middleware in Express is a function with the signature (req, res, next) that runs inside the request pipeline. It receives the request object and the response object, and it can read or modify them: for example, adding req.usuario after authenticating. From there it has two valid options: finish the response with something like res.json, or call next() to hand control to the next middleware or to the route. If it does neither, the request hangs. I register them with app.use, and I use them for cross-cutting concerns like logging, authentication, or body parsing, so I avoid repeating the same logic in every route."

Quick challenge

What does this code print when a request arrives at GET /, and why?

app.use((req, res, next) => {
  console.log("A");
  next();
  console.log("B");
});

app.get("/", (req, res) => {
  console.log("C");
  res.send("ok");
});
See answer

It prints A, then C, then B.

The middleware prints "A" and calls next(). That next() immediately runs the next link in the pipeline, which is the GET / route, which prints "C" and responds. When the route finishes, control returns to the line that came after next() in the middleware, which is why "B" is printed at the end. The key is to understand that next() doesn't "end" the middleware: it runs what comes next and then returns.