DEV Community

Cover image for What is Middleware in Express and How It Works
Janmejai Singh
Janmejai Singh

Posted on

What is Middleware in Express and How It Works

What Is Middleware in Express, and How Does It Actually Work?

If you've spent any time with Express.js, you've probably written code like app.use(something) dozens of times without fully stopping to ask: what is that, exactly? It's not a route. It's not quite a controller. It just... sits there, quietly doing something to every request that passes through.

That "something" is middleware, and it's arguably the single most important concept to understand if you want Express to stop feeling like a black box. Once it clicks, you'll start seeing the entire framework differently — as a pipeline you control, one checkpoint at a time.

Middleware as a Checkpoint Between Request and Response

Here's the simplest way to picture it: imagine an airport. A passenger (the request) doesn't walk straight from the front door to their airplane seat (the response). They pass through a series of checkpoints along the way — check-in, security screening, passport control, boarding gate. Each checkpoint inspects, modifies, logs, or sometimes outright stops the passenger before letting them continue to the next one.

Middleware in Express is exactly that: a function sitting between the incoming request and the final response, given a chance to inspect or modify things, run some logic, and then either pass the request along to the next checkpoint or stop it right there.

Formally, an Express middleware function is just a function with this shape:

function myMiddleware(req, res, next) {
  // do something with req or res
  next(); // pass control to the next checkpoint
}
Enter fullscreen mode Exit fullscreen mode

Three ingredients, every time: the request object, the response object, and a special function called next. That third one is doing more work than it looks like, and we'll come back to it shortly.

Where Middleware Sits in the Request Lifecycle

When a request hits your Express app, it doesn't go straight to a route handler. It travels through a pipeline — a sequence of middleware functions, registered in the order you defined them, each one getting a turn before the request (assuming it survives that long) finally reaches the route handler responsible for sending a response.

Picture the flow like this:

Incoming Request
      |
      v
 [ Middleware 1 ]  --(calls next())-->
      |
      v
 [ Middleware 2 ]  --(calls next())-->
      |
      v
 [ Route Handler ] --(sends response)
      |
      v
   Response sent back to client
Enter fullscreen mode Exit fullscreen mode

This is why Express is often described as having a request pipeline — your request flows through a chain of functions, and each one gets to decide: keep going, stop here, or branch off entirely (say, by sending an error response early).

Types of Middleware in Express

Not all middleware is registered the same way or applies to the same scope. Express groups middleware into a few practical categories.

Application-Level Middleware

This is middleware attached directly to your main app object using app.use() or app.METHOD() (like app.get()). It applies broadly — often to every request that comes into your app, unless you scope it to a specific path.

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

app.use((req, res, next) => {
  console.log(`${req.method} request to ${req.url}`);
  next();
});
Enter fullscreen mode Exit fullscreen mode

This is your go-to for things that should happen on basically every request — logging, setting common headers, parsing request bodies, and so on.

Router-Level Middleware

Sometimes you don't want middleware applying app-wide — you want it scoped to a specific group of routes. That's where express.Router() comes in. You attach middleware to a router instance instead of the whole app, and it only runs for requests handled by that router.

const router = express.Router();

router.use((req, res, next) => {
  console.log('A request hit the /users router');
  next();
});

router.get('/users/:id', (req, res) => {
  res.send('User profile');
});
Enter fullscreen mode Exit fullscreen mode

This is especially handy for feature-specific logic — like requiring authentication only on /admin routes, without affecting your public-facing pages at all.

Built-In Middleware

Express also ships with a small set of middleware functions out of the box, so you don't have to reinvent common functionality. The most commonly used one is express.json(), which parses incoming JSON request bodies and makes them available on req.body.

app.use(express.json());
Enter fullscreen mode Exit fullscreen mode

There's also express.static(), which serves static files (images, CSS, client-side JS) directly from a folder, without you needing to write any custom file-serving logic yourself.

Execution Order of Middleware

This part trips up a lot of beginners, so it's worth saying plainly: middleware runs in the exact order you register it, top to bottom. Express doesn't reorder, prioritize, or guess — it simply walks down your stack of app.use() and route definitions, one at a time.

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

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

app.get('/dashboard', (req, res) => {
  console.log('3: Route handler');
  res.send('Welcome to your dashboard');
});
Enter fullscreen mode Exit fullscreen mode

For a request to /dashboard, the console output will always read 1, then 2, then 3 — never out of order. This is exactly like the airport checkpoints: you can't reach passport control before you've gone through security, simply because of the order the checkpoints are physically placed in.

This ordering matters a lot in practice. If you register a body-parsing middleware after a route handler that depends on req.body, that route will see undefined — because the parser never got a chance to run first.

The Role of next()

Now, back to that mysterious third argument. The next() function is what makes the pipeline actually move. Calling next() tells Express: "I'm done with my part — pass this request on to whatever's next in line."

If a middleware function never calls next() (and never sends a response either), the request simply hangs forever. The client's browser or app sits there waiting, because nothing ever told Express to move on or respond.

This gives middleware real power, though — it can also choose not to call next(), and instead end the chain itself:

function requireAuth(req, res, next) {
  if (!req.headers.authorization) {
    return res.status(401).send('Unauthorized'); // chain stops here
  }
  next(); // only continues if authorized
}
Enter fullscreen mode Exit fullscreen mode

This is the checkpoint analogy in its purest form: a security checkpoint that decides a passenger isn't allowed through simply doesn't let them proceed — it stops them right there, instead of waving them on to the gate.

Real-World Examples of Middleware in Action

Theory aside, here's where middleware earns its keep in almost every real Express app.

Logging

Tracking what's happening in your app — which routes are getting hit, by what method, when — is a textbook middleware job, since it needs to run on every request without cluttering up your actual route logic.

app.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next();
});
Enter fullscreen mode Exit fullscreen mode

Authentication

Checking whether a user is logged in (or has the right permissions) before letting them reach a protected route is one of the most common real-world uses of middleware, exactly like the requireAuth example above. Instead of repeating that check inside every single route handler, you write it once and apply it wherever it's needed.

Request Validation

Before your route handler even touches incoming data, middleware can check that it's actually shaped the way you expect — required fields present, correct types, no garbage data sneaking through.

function validateSignup(req, res, next) {
  const { email, password } = req.body;
  if (!email || !password) {
    return res.status(400).send('Email and password are required');
  }
  next();
}

app.post('/signup', validateSignup, (req, res) => {
  res.send('Account created!');
});
Enter fullscreen mode Exit fullscreen mode

Notice that last example — middleware doesn't only get registered with app.use(). You can also slot it directly into a single route's argument list, applying it to just that one path.

Visualizing It All Together

Put the pieces together, and a typical request through a well-structured Express app looks like a chain:

Request
  → Logging middleware (records the request)
  → express.json() (parses the body)
  → Auth middleware (checks if user is allowed through)
  → Validation middleware (checks the data shape)
  → Route handler (does the actual work, sends response)
Enter fullscreen mode Exit fullscreen mode

Each link in that chain does exactly one job, calls next() when it's satisfied, and trusts the next link to handle its own responsibility. That's the real elegance of middleware — it lets you break a complex request-handling process into small, focused, reusable pieces instead of one giant tangled route handler trying to do everything at once.

Wrapping Up

Middleware is Express's way of turning request handling into a pipeline of checkpoints rather than one big monolithic function. Application-level middleware applies broadly, router-level middleware applies to a specific group of routes, and built-in middleware handles common needs out of the box — and all of it runs strictly in the order you register it, moving forward only when next() is called. Once you start thinking in terms of "what checkpoint does this logic belong at," patterns like logging, authentication, and validation stop feeling like separate, special-case features and start feeling like exactly what they are: just more middleware in the chain.


Curious about error-handling middleware, or how middleware works differently in newer frameworks built on Express? Drop a comment below — I read every one.

Top comments (0)