DEV Community

Bhupesh Chandra Joshi
Bhupesh Chandra Joshi

Posted on

What is Middleware in Express and How It Works

If you've spent any time with Node.js, you've probably heard the word middleware thrown around like everyone already agrees on what it means. The truth is, middleware is one of those concepts that sounds abstract until you see it in action — and once you do, Express suddenly makes a lot more sense.

In this article, we'll break middleware down the way I wish someone had explained it to me when I started: with analogies, diagrams, and real code you'd actually write on the job.


  1. So, What Is Middleware?

In Express, middleware is just a function that sits between the incoming request and the final response.

Think of it as a checkpoint. Every request that hits your server has to walk down a hallway of checkpoints before it reaches the route handler that actually does the work. Each checkpoint can:

  • Inspect the request
  • Modify the request or response
  • End the request early (e.g., reject it)
  • Or pass it along to the next checkpoint

That's it. No magic. A middleware function in Express has this signature:

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

Three arguments: req, res, and next. The next is what makes the chain move forward.


  1. Where Middleware Sits in the Request Lifecycle

Here's the mental model I want you to lock in:

Client Request
      │
      ▼
┌─────────────┐
│ Middleware 1│  ── logging
└─────────────┘
      │ next()
      ▼
┌─────────────┐
│ Middleware 2│  ── authentication
└─────────────┘
      │ next()
      ▼
┌─────────────┐
│ Middleware 3│  ── validation
└─────────────┘
      │ next()
      ▼
┌─────────────┐
│ Route Handler│ ── business logic
└─────────────┘
      │
      ▼
   Response
Enter fullscreen mode Exit fullscreen mode

Every request flows through this pipeline. Middleware is the plumbing; route handlers are the destination.


  1. Types of Middleware

Express groups middleware into a few categories. You don't need to memorize them — just know what each one looks like.

a) Application-Level Middleware

Bound to your app instance. Runs for every request (or every request matching a path).

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

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

b) Router-Level Middleware

Same idea, but scoped to an express.Router() instance. Useful when you want middleware that only affects a section of your app — say, all /admin routes.

const router = express.Router();

router.use((req, res, next) => {
  console.log("Admin route hit");
  next();
});

router.get("/dashboard", (req, res) => {
  res.send("Welcome, admin");
});

app.use("/admin", router);
Enter fullscreen mode Exit fullscreen mode

c) Built-in Middleware

Express ships with a few out of the box. The two you'll use constantly:

app.use(express.json());                       // parses JSON bodies
app.use(express.urlencoded({ extended: true })); // parses form bodies
app.use(express.static("public"));             // serves static files
Enter fullscreen mode Exit fullscreen mode

d) Third-Party Middleware

Installed from npm. Examples: cors, helmet, morgan, cookie-parser.

const cors = require("cors");
app.use(cors());
Enter fullscreen mode Exit fullscreen mode

e) Error-Handling Middleware

Same idea, but with four arguments. Express recognizes the signature and treats it as an error handler.

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: "Something broke" });
});
Enter fullscreen mode Exit fullscreen mode

  1. Execution Order Matters — A Lot

This is the part that trips people up. Middleware runs in the order you register it. Top to bottom. No exceptions.

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

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

app.get("/", (req, res) => {
  console.log("3");
  res.send("Done");
});
Enter fullscreen mode Exit fullscreen mode

Hit / and you'll see:

1
2
3
Enter fullscreen mode Exit fullscreen mode

If you put your auth middleware after your route handler, it will never protect anything. Order is everything.


  1. The Role of next()

next() is the baton in a relay race. If a middleware doesn't call it (and doesn't send a response), the request just hangs until it times out. This is one of the most common Express bugs.

You have three choices inside any middleware:

  1. Call next() → pass control to the next middleware.
  2. Send a response (res.send, res.json, etc.) → end the cycle.
  3. Call next(err) → skip ahead to the nearest error-handling middleware.
app.use((req, res, next) => {
  if (!req.headers.authorization) {
    return next(new Error("Unauthorized")); // jumps to error handler
  }
  next();
});
Enter fullscreen mode Exit fullscreen mode

  1. Real-World Examples

Let's stop being theoretical. Here are three middleware patterns you'll write in real projects.

Example 1 — Logging

app.use((req, res, next) => {
  const start = Date.now();
  res.on("finish", () => {
    const ms = Date.now() - start;
    console.log(`${req.method} ${req.url}${res.statusCode} (${ms}ms)`);
  });
  next();
});
Enter fullscreen mode Exit fullscreen mode

A poor man's morgan. Useful for quick debugging.

Example 2 — Authentication

function requireAuth(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).json({ error: "No token" });

  try {
    req.user = verifyToken(token); // attach user to request
    next();
  } catch {
    res.status(401).json({ error: "Invalid token" });
  }
}

app.get("/profile", requireAuth, (req, res) => {
  res.json({ user: req.user });
});
Enter fullscreen mode Exit fullscreen mode

Notice how middleware can be applied to a single route, not just globally.

Example 3 — Request Validation

function validateUser(req, res, next) {
  const { email, password } = req.body;
  if (!email || !password) {
    return res.status(400).json({ error: "Email and password required" });
  }
  next();
}

app.post("/signup", validateUser, (req, res) => {
  // safe to assume email/password exist
  res.json({ message: "User created" });
});
Enter fullscreen mode Exit fullscreen mode

In production you'd reach for zod or joi, but the pattern is the same.


  1. Putting It All Together

Here's a tiny but realistic Express app showing the full pipeline:

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

// 1. Built-in
app.use(express.json());

// 2. Application-level (logging)
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next();
});

// 3. Auth middleware on a specific route
app.get("/dashboard", requireAuth, (req, res) => {
  res.send(`Welcome, ${req.user.name}`);
});

// 4. Error handler — always last
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message });
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Read it top to bottom. That's exactly the order a request travels.


  1. Mental Model to Remember

If you forget everything else, remember this:

Express is just a pipeline of functions. Middleware is each function in the pipe. next() is what keeps water flowing.

Once that clicks, everything else — auth, logging, validation, error handling — is just a variation on the same pattern.


Wrapping Up

Middleware isn't a framework feature so much as a philosophy: small, composable functions that each do one thing, chained together to handle a request. Master it and you'll write Express apps that are easier to read, easier to debug, and easier to extend.

Next time you reach for a giant route handler doing five things at once — stop. Ask yourself: could three small middlewares do this better? Usually, the answer is yes.

Happy shipping. 🚀


Top comments (0)