Middleware Concept

intermediate express middleware next pipeline

If you understand middleware, you understand Express. Everything in Express — routing, body parsing, auth, error handling — is built on this one idea.

In simple language — middleware is a function that runs between the request coming in and the response going out. It gets the request and response objects, and a next function. It can:

  1. Run any code (logging, auth checks)
  2. Modify req or res (attach req.user, set headers)
  3. End the request (send a response itself)
  4. Call next() to pass to the next middleware

The whole Express app is a pipeline of these functions executed in order.

The signature

function middleware(req, res, next) {
  // ... do something
  next(); // pass to the next middleware
}

That’s it. Three arguments. Call next() when done.

A simple example

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

// Middleware 1: log every request
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next();
});

// Middleware 2: attach a timestamp
app.use((req, res, next) => {
  req.requestTime = Date.now();
  next();
});

// Route handler (also middleware, just the last one)
app.get('/', (req, res) => {
  res.json({ at: req.requestTime });
});

app.listen(3000);

When GET / comes in:

  1. Logger runs, calls next()
  2. Timestamp middleware runs, calls next()
  3. Route handler runs, sends response
Middleware pipeline
req in
logger
next()→
body parser
next()→
auth
next()→
handler
res out
Any middleware can short-circuit by sending a response instead of calling next()

ORDER MATTERS (this is the #1 gotcha)

Express runs middleware in the order they’re registered. If body parser comes after the route, the route sees req.body === undefined:

// BROKEN
app.post('/users', (req, res) => {
  res.json(req.body); // undefined!
});
app.use(express.json()); // too late
// CORRECT
app.use(express.json());
app.post('/users', (req, res) => {
  res.json(req.body); // works
});

Rule of thumb — register middleware in this order:

  1. Logging
  2. Security (helmet, cors)
  3. Body parsing (express.json, express.urlencoded)
  4. Auth
  5. Routes
  6. 404 handler
  7. Error handler (last, always)

Three ways to use middleware

// 1. App-level — runs for every request
app.use(logger);

// 2. Path-specific — only for requests starting with /api
app.use('/api', apiMiddleware);

// 3. Route-specific — only for this one route
app.get('/admin', requireAuth, (req, res) => { ... });

Short-circuiting

A middleware can end the request itself by sending a response and NOT calling next():

const requireAuth = (req, res, next) => {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next(); // only continues if authorized
};

The return is critical — without it, both the response AND next() could fire, which causes “Cannot set headers after they are sent.”

Error-handling middleware

A special type — four arguments. Express recognizes it by arity:

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'Internal server error' });
});

To send an error into this, call next(err) with an argument:

app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await db.findUser(req.params.id);
    if (!user) return next(new Error('Not found'));
    res.json(user);
  } catch (err) {
    next(err); // jumps to error middleware
  }
});

Always register the error handler last, after all routes.

Reusing middleware

Middleware is just a function — we can publish it to npm, import from anywhere, share across projects. That’s why the Express ecosystem is huge: cors, helmet, morgan, compression, cookie-parser, express-rate-limit are all just middlewares.

The mental model

“An Express app is an ordered list of functions that each get a chance to read/modify the request, send a response, or pass control to the next one.”

If you internalize that, the rest of Express falls out naturally — routes are just middlewares matched by path/method, error handlers are middlewares with 4 args, routers are sub-pipelines.