Skip to main content

Command Palette

Search for a command to run...

What is Middleware in Express and How It Works

Updated
6 min read
What is Middleware in Express and How It Works
M

Full-stack developer with a good foundation in frontend, now specializing in backend development. Passionate about building efficient, scalable systems and continuously sharpening my problem-solving skills. Always learning, always evolving.

A few years back, I was debugging an Express API where a simple GET /users request was returning a 401 error. The route handler looked fine. The bug wasn’t there. It turned out three different functions were running before the request even reached the route, and one of them was silently rejecting the request. That’s when the middleware chain became very real.

Once you see it, you stop thinking in terms of “routes” and start thinking in terms of a pipeline.


What Middleware Is in Express

At its core, middleware in Express is just a function that runs between receiving a request and sending a response.

More precisely:

  • It has access to:

    • req (request)

    • res (response)

    • next (control function)

  • It can:

    • Execute logic

    • Modify req or res

    • End the request

    • Pass control to the next function (expressjs.com)

That sounds abstract, but in practice:

app.use((req, res, next) => {
  console.log('Incoming request');
  next();
});

This function runs before your route handler. It doesn’t care which route. It intercepts everything.

The key idea:

Middleware is not “extra logic”. It is part of the request handling pipeline.

Express itself is basically a chain of middleware functions executed in sequence.


Where Middleware Sits in the Request Lifecycle

When a request hits your server, it doesn’t go straight to the route handler.

It flows through a pipeline.

Diagram: Request Flow

Client → Middleware 1 → Middleware 2 → Middleware 3 → Route Handler → Response

Step-by-step:

  1. Request arrives at Express

  2. First middleware runs

  3. It either:

    • Ends the request

    • Or calls next()

  4. Next middleware runs

  5. Eventually, the route handler executes

  6. Response is sent back

Express processes middleware in sequence, one after another (GeeksforGeeks).

This pipeline model is the mental model you want. Not “Express calls my route”, but:

“The request flows through layers, and each layer decides what happens next.”


Types of Middleware

You’ll encounter a few practical categories.

1. Application-level middleware

Defined using:

app.use(...)

Runs for all routes or a subset of paths.

Use cases:

  • Logging

  • Authentication

  • Parsing request bodies

Example:

app.use((req, res, next) => {
  console.log(req.method, req.url);
  next();
});

2. Router-level middleware

Attached to a specific router.

const router = express.Router();

router.use((req, res, next) => {
  console.log('Router middleware');
  next();
});

Use this when:

  • You want isolation

  • You’re building modular APIs


3. Built-in middleware

Provided by Express itself.

Example:

app.use(express.json());

This parses JSON request bodies and attaches them to req.body (expressjs.com).

Other examples:

  • express.urlencoded()

  • express.static()


The practical takeaway:

Middleware is how you compose behavior. Not by writing large route handlers, but by layering small functions.


Execution Order of Middleware

This is where most bugs come from.

Middleware runs in the order you define it (expressjs.com).

Example

app.use((req, res, next) => {
  console.log('First');
  next();
});

app.use((req, res, next) => {
  console.log('Second');
  next();
});

app.get('/', (req, res) => {
  res.send('Done');
});

Execution order:

First → Second → Route Handler

Now the subtle bug

app.get('/', (req, res) => {
  res.send(req.user);
});

app.use((req, res, next) => {
  req.user = { name: 'Mohammad' };
  next();
});

This will fail.

Why?

  • The route runs before the middleware

  • req.user is undefined

This is not a syntax issue. It’s a pipeline ordering problem.

If a middleware produces data, it must run before anything that consumes it.


Role of next() Function

next() is what moves the request forward.

Think of it as:

“I’m done. Pass control to the next layer.”

Basic behavior

app.use((req, res, next) => {
  console.log('Step 1');
  next();
});

app.use((req, res, next) => {
  console.log('Step 2');
  next();
});

Flow:

Step 1 → Step 2

If you don’t call next()

The request stops there.

app.use((req, res, next) => {
  console.log('Blocked');
  // no next()
});

Result:

  • Request hangs

  • No response is sent

Express explicitly requires that if middleware does not end the request, it must call next() (Stack Overflow).

Special behavior

You can also:

next('route');

This skips remaining middleware in the current stack and jumps to the next route.

Use it carefully. It’s easy to make control flow unpredictable.


Real-World Middleware Examples

This is where middleware actually earns its place.

1. Logging middleware

app.use((req, res, next) => {
  console.log(`\({req.method} \){req.url}`);
  next();
});

Purpose:

  • Observability

  • Debugging request flow

In production, you’ll use tools like morgan for structured logs (expressjs.com).


2. Authentication middleware

app.use((req, res, next) => {
  const token = req.headers.authorization;

  if (!token) {
    return res.status(401).send('Unauthorized');
  }

  req.user = verifyToken(token);
  next();
});

Purpose:

  • Protect routes

  • Attach user context

Important detail:

  • This must run before protected routes

3. Request validation middleware

app.post('/users', (req, res, next) => {
  if (!req.body.email) {
    return res.status(400).send('Email required');
  }
  next();
}, (req, res) => {
  res.send('User created');
});

Purpose:

  • Reject bad input early

  • Keep route handlers clean


Diagram: Middleware Execution Chain

Request
   ↓
[ Logger Middleware ]
   ↓ (next)
[ Auth Middleware ]
   ↓ (next)
[ Validation Middleware ]
   ↓ (next)
[ Route Handler ]
   ↓
Response

Key observations:

  • Each step decides:

    • Continue

    • Or terminate

  • Order defines behavior

  • Missing next() breaks the chain


Final Perspective

If you treat middleware as “helper functions”, you’ll eventually hit confusion.

If you treat it as a pipeline of responsibility, things become predictable:

  • Each middleware has a single responsibility

  • Order defines correctness

  • next() defines flow control

In real systems, most bugs around Express are not about syntax. They are about:

  • Incorrect ordering

  • Missing next()

  • Middleware doing too much

The clean approach:

Build small, composable middleware layers and make the request flow explicit.

That’s how you keep Express apps maintainable once they grow beyond a few routes.