Just added: Algorithms you can run and practice
Updated Jul 10, 2026 Edit on GitHub

Middleware

The secret that makes Express click: there is no separate "middleware feature" bolted on the side. Middleware is Express. Routing is middleware. Body parsing is middleware. Auth, logging, error handling - all the same shape. Once you see it, the framework stops being a pile of methods to memorize and becomes one idea you understand all the way down.

The mental model: a chain of (req, res, next) functions

📝 Picture a request walking through a hallway of doors. Each door is a function with three things: req (what came in), res (what you'll send back), and next (a doorknob that opens the next door).

At each door, the function does one of three things:

  1. Read or modify req/res, then call next() to pass the request along.
  2. End the response itself (res.send(...), res.json(...)) - the request stops here.
  3. Call next() with nothing changed, just to hand off.

A middleware function looks exactly like this:

function myMiddleware(req, res, next) {
  // ...do something with req or res...
  next(); // open the next door
}

What just happened: three parameters, and a decision about whether to call next() - the shape of every middleware in Express. A route handler like app.get('/tasks', (req, res) => ...) is the same thing; it just happens to be the last door, so it sends a response instead of calling next(). Routes are middleware bound to a method and a path.

💡 If you read Build a Server with node:http, you already met (req, res) and "call the next function to keep going." Express didn't invent this - it formalized it.

Writing your first middleware: a request logger

The classic first middleware logs every request: method, URL, status, and duration.

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

function logger(req, res, next) {
  const start = Date.now();
  res.on('finish', () => {
    console.log(`${req.method} ${req.url} ${res.statusCode} ${Date.now() - start}ms`);
  });
  next(); // hand off to the next middleware
}

app.use(logger); // runs for EVERY request

app.get('/tasks', (req, res) => {
  res.json([{ id: 1, title: 'Write the docs' }]);
});

app.listen(3000, () => console.log('http://localhost:3000'));

What just happened: app.use(logger) registers logger to run at the start of every request. We record start, then attach a one-time listener to res's 'finish' event - fired when the response is fully sent - so we can log the real status code and duration. logger calls next() immediately; it doesn't wait for finish. Its job in the chain is to step aside and let the request continue. Hit GET /tasks and you'll see GET /tasks 200 2ms.

Three places you can register middleware

app.use(logger);                          // GLOBAL: every request
app.use('/admin', requireLogin);          // PATH-SCOPED: only paths under /admin
app.get('/tasks/:id', loadTask, handler); // PER-ROUTE: only this route, in this order

What just happened: same mechanism, three reaches. app.use(fn) runs on everything. app.use('/admin', fn) runs only when the path starts with /admin. Listing functions inside a route runs them left to right for that one route. The middleware doesn't change - only where you mount it does.

⚠️ Order matters (this is the part that bites everyone)

Middleware runs in the order you register it, top to bottom. Getting this wrong is the most common Express bug there is.

Trap 1: the parser must come before the routes that need it. express.json() reads the request body into req.body. Register your routes before it, and req.body is undefined there.

// ❌ WRONG - route runs before the body is parsed
app.post('/tasks', (req, res) => {
  res.json(req.body); // undefined - nothing parsed it yet
});
app.use(express.json());

// ✅ RIGHT - parse first, then route
app.use(express.json());
app.post('/tasks', (req, res) => {
  res.json(req.body); // { title: "..." } - parsed and ready
});

What just happened: in the wrong version, the handler runs before express.json() has parsed anything, so req.body is undefined. In the right version, parsing happens earlier in the chain, so by the time your handler runs, req.body is already populated. Order on the page = order at runtime.

Trap 2: forgetting next() hangs the request. A middleware that neither responds nor calls next() is a closed door with no knob - the request stands there forever, until the client times out.

// ❌ This middleware silently hangs every request
app.use((req, res, next) => {
  console.log('checking request...');
  // no next(), no res.send() - DEAD END
});

What just happened: this is the number-one beginner bug. The function logs and returns, but never calls next() or sends a response, so Express has no way to know it's done. Every middleware must do exactly one of two things: respond, or call next(). If a request hangs for no reason, check for a missing next() first.

Built-in and third-party middleware

You rarely write parsers and security headers yourself - you reach for middleware that already exists.

Built into Express:

  • express.json() - parses JSON request bodies into req.body.
  • express.urlencoded({ extended: true }) - parses HTML form submissions into req.body.
  • express.static('public') - serves files from a folder (images, CSS, the front end).

Popular third-party packages (install with npm, then app.use(...) them):

  • cors - adds the headers browsers need to allow cross-origin requests.
  • morgan - a polished request logger (like ours, but configurable).
  • helmet - sets a bundle of security-related HTTP headers.
const cors = require('cors');
const helmet = require('helmet');

app.use(helmet());          // security headers on every response
app.use(cors());            // allow cross-origin requests
app.use(express.json());    // parse JSON bodies
app.use(morgan('dev'));     // log requests

What just happened: each line plugs a ready-made middleware into the chain, listed near the top so they run early - security headers and CORS should apply to every response, and express.json() must come before any route reading req.body (Trap 1). Helmet, cors, json, a logger: the boring, sensible opening for most real Express apps.

Passing data down the chain with req

Middleware can attach things to req, and every later function in the chain can read them. The textbook case is authentication: one middleware checks who's calling and stashes the user on req.

function requireAuth(req, res, next) {
  const token = req.headers['authorization'];
  if (!token) {
    return res.status(401).json({ error: 'Not authenticated' });
    // note: we DON'T call next() - the request stops here
  }
  req.user = { id: 1, name: 'Ada' }; // in real life: verify the token
  next(); // authenticated - continue to the route
}

app.use(express.json());
app.use(requireAuth);

app.get('/tasks', (req, res) => {
  res.json({ owner: req.user.name, tasks: [] }); // req.user came from the middleware
});

What just happened: no header means an unauthenticated request, so requireAuth responds 401 and stops - note there's no next() after it. A header sets req.user and calls next(), so by the time /tasks runs, req.user is populated and the route trusts that auth already happened upstream. Two halves of the pattern: reject and stop, or enrich req and continue. The return matters

  • without it, the code would respond and fall through to next(), trying to send two responses.

Recap

  • Middleware is a (req, res, next) function in a chain. It can read/modify req/res, end the response, or call next(). Routes are middleware bound to a method and path.
  • Register at three scopes: global (app.use(fn)), path-scoped (app.use('/admin', fn)), or per-route (app.get(path, mw, handler)).
  • Order matters. Middleware runs top to bottom. express.json() must come before any route that reads req.body.
  • A middleware that neither responds nor calls next() hangs the request - the #1 beginner bug.
  • Use built-ins (express.json, express.urlencoded, express.static) and third-party packages (cors, morgan, helmet) instead of writing your own.
  • Share work down the chain by attaching to req (e.g. an auth middleware sets req.user); reject with res.status(401) and don't call next().

Quick check

[
  {
    "q": "What are the three parameters of a standard Express middleware function?",
    "choices": ["req, res, done", "request, response, callback", "req, res, next", "app, req, res"],
    "answer": 2,
    "explain": "Standard middleware is (req, res, next): read/modify req and res, then call next() to pass control along."
  },
  {
    "q": "You register your POST route before express.json(). What happens to req.body in that route?",
    "choices": ["It contains the parsed JSON", "It is undefined because nothing parsed the body yet", "It throws an error", "Express auto-reorders the middleware for you"],
    "answer": 1,
    "explain": "Middleware runs in registration order. If express.json() is registered after the route, the route runs first and req.body is undefined."
  },
  {
    "q": "A middleware logs a message but never calls next() and never sends a response. What is the result?",
    "choices": ["The next middleware runs anyway", "Express sends an automatic 404", "The request hangs until it times out", "The response is sent with status 200"],
    "answer": 2,
    "explain": "Every middleware must either respond or call next(). Doing neither leaves the request with nowhere to go, so it hangs - the most common beginner bug."
  }
]

Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.

Check your understanding 3 questions

1. What are the three parameters of a standard Express middleware function?

2. You register your POST route before express.json(). What happens to req.body in that route?

3. A middleware logs a message but never calls next() and never sends a response. What is the result?