Skip to main content

Command Palette

Search for a command to run...

Middleware in Express

What is Middleware in Express and How It Works

Published
โ€ข6 min read
Middleware in Express
S

Frontend Developer ๐Ÿ’ป | Fueled by curiosity and Tea โ˜• | Always learning and exploring new technologies.

Every HTTP request that hits your Express server has a journey to make from the moment it arrives to the moment a response goes back. Middleware is what lives in between.

The Simplest Definition

Middleware is a function that runs between the incoming request and the final route handler.

That's it. Any function in Express that receives req, res, and next is middleware.

function myMiddleware(req, res, next) {
  // do something with the request or response
  next(); // pass control to the next function
}

Three parameters. That's the signature. The next function is what makes it middleware rather than a route handler calling next() says "I'm done, pass this request to whoever is registered next."

Middleware in the Request Lifecycle

Think of every Express request as going through a pipeline โ€” a series of checkpoints before it reaches the final destination. Each checkpoint can inspect the request, modify it, add data to it, block it entirely, or pass it through.

Incoming request
      โ†“
  [Middleware 1]   โ† logging: log method + URL, call next()
      โ†“
  [Middleware 2]   โ† auth: check token, call next() or return 401
      โ†“
  [Middleware 3]   โ† body parser: parse JSON into req.body, call next()
      โ†“
  [Route handler]  โ† do the actual work, send the response
      โ†“
Outgoing response

Every function in that chain gets the same req and res objects. If middleware attaches something to req say, req.user = decodedToken every function after it in the chain can access req.user. This is how authentication middleware passes the authenticated user to a route handler.

The next() Function

next() is what keeps the chain alive. When middleware calls next(), Express moves to the next registered function. When middleware doesn't call next() (and doesn't send a response either), the request just hangs. The client waits forever. This is one of the most common Express bugs a middleware that silently swallows requests.

// Correct โ€” always either call next() or send a response
function checkHeader(req, res, next) {
  if (!req.headers['x-api-key']) {
    return res.status(401).json({ error: 'API key required' });
    // we're done โ€” no next() needed, response is sent
  }
  next(); // key exists โ€” move on
}

Two exits: send a response (and stop), or call next() (and continue). Every middleware must do one of the two.

What most tutorials don't explain about next(): you can pass an argument to next(). Passing anything (by convention, an error object) skips all remaining regular middleware and jumps directly to an error-handling middleware:

function riskyOperation(req, res, next) {
  try {
    const result = doSomethingDangerous();
    next();
  } catch (err) {
    next(err); // skip to error handler
  }
}

// Error-handling middleware: 4 parameters, always (err, req, res, next)
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'Something went wrong' });
});

The four-parameter signature is Express's convention for error handlers. Express identifies them specifically by the number of parameters not by name, not by position, just by arity. If you write (err, req, res, next), Express treats it as an error handler and only calls it when next(err) is called somewhere upstream.

Types of Middleware

Application-level middleware

Registered on the app object with app.use(). Runs for every matching request.

// Runs for every request, every path
app.use((req, res, next) => {
  console.log(`\({req.method} \){req.url}`);
  next();
});

// Runs only for requests to /api/*
app.use('/api', (req, res, next) => {
  console.log('API request received');
  next();
});

Router-level middleware

The same concept, but attached to an express.Router() instance instead of the app. This is how you apply middleware only to a subset of routes:

const router = express.Router();

// Applies to all routes in this router
router.use((req, res, next) => {
  console.log('Router-level middleware');
  next();
});

router.get('/profile', (req, res) => {
  res.json({ user: req.user });
});

app.use('/users', router);

If you mount this router at /users, the middleware only runs for requests to /users/*.

Built-in middleware

Express ships with a few middleware functions out of the box โ€” no extra packages needed:

express.json() โ€” parses request bodies with Content-Type: application/json. Makes them available as req.body.

app.use(express.json());

express.urlencoded({ extended: true }) โ€” parses URL-encoded form data (the kind HTML forms submit). Makes it available as req.body.

app.use(express.urlencoded({ extended: true }));

express.static(directory) โ€” serves static files (HTML, CSS, images, JS) from a folder.

app.use(express.static('public')); // serves files from ./public

These are the ones you'll register on almost every Express app. There's also express.raw() and express.text() for other content types, but they're less commonly needed.

Execution Order:

Middleware runs in the exact order it's registered. There are no priorities, no weights just top to bottom.

app.use(loggerMiddleware);      // 1 โ€” runs first
app.use(express.json());        // 2 โ€” then body parsing
app.use(authMiddleware);        // 3 โ€” then authentication

app.get('/dashboard', (req, res) => {
  // 4 โ€” route handler (only if all middleware called next())
  res.json({ page: 'dashboard', user: req.user });
});

A request to GET /dashboard runs through all four in sequence. If authMiddleware returns a 401, the route handler never runs.

What happens with multiple route handlers matching the same path? They run in order too. This is less obvious but occasionally useful:

app.get('/users/:id', loadUser);     // attaches user to req
app.get('/users/:id', checkAccess);  // verifies req.user can see this user
app.get('/users/:id', sendResponse); // sends the response

Three separate handler registrations for the same route. Each calls next() to pass to the next. This is called "route chaining" and it's a valid (if uncommon) pattern for decomposing complex route logic.

Real-World Examples

Logging middleware

function logger(req, res, next) {
  const start = Date.now();

  // Intercept res.end to know when the response was sent
  const originalEnd = res.end.bind(res);
  res.end = function (...args) {
    const duration = Date.now() - start;
    console.log(`\({req.method} \){req.url} \({res.statusCode} โ€” \){duration}ms`);
    return originalEnd(...args);
  };

  next();
}

app.use(logger);

Middleware is a conveyor belt. Each station on the belt can inspect the item, stamp it, reject it, or pass it along. The req and res objects are the item they get passed from station to station. next() is the button that moves the belt forward.

Some stations are always running (global middleware). Some only activate for certain routes. Some are fail-safes at the end (error handlers). Together, they turn a raw HTTP request into a handled, validated, authenticated, logged transaction.

Once you see Express this way, the whole framework clicks.