Home/Blog/Backend/Middleware in Express.js
Backend8 min readAug 25, 2026

Middleware in Express.js

You've used app.use(express.json()) a hundred times without asking what it actually does. This blog breaks down what middleware really is, how requests flow through a stack of them in order, and how to write your own, including the error-handling kind most tutorials skip.

98 views0 likes0 comments0 bookmarks

You've already used middleware. You just didn't know that's what it was called.

If you've written even one Express app, you've typed something like this:

js

Js
app.use(express.json());

You probably knew it lets you read req.body, and moved on. Fair enough, it works. But that one line is your first encounter with middleware, and once you understand what's actually happening there, a lot of Express starts making more sense, including why some bugs happen (like req.body being undefined) and why route order in your file actually matters.

The problem middleware solves

Every request that hits your Express server needs to go through some common processing before it reaches the actual logic you care about. Things like: is this request's body valid JSON? Is the user logged in? Should this request even be allowed to reach this route, or should it get rejected? Do we want to log that this request happened?

Without a shared way to handle this, you'd end up writing the same checks inside every single route handler.

js

Js
app.get("/dashboard", (req, res) => {
  if (!req.headers.authorization) {
    return res.status(401).json({ message: "Not authorized" });
  }
  // ... actual dashboard logic
});

app.get("/settings", (req, res) => {
  if (!req.headers.authorization) {
    return res.status(401).json({ message: "Not authorized" });
  }
  // ... actual settings logic
});

That's repetitive, error-prone, and gets worse as your app grows. If you have twenty protected routes, you have twenty copies of that same check, and if you ever need to change how auth works, you're editing twenty places instead of one.

Express solves this with middleware: functions that sit in between the incoming request and your final route handler, each one able to inspect, modify, or reject the request before passing it along.

[IMAGE: Diagram showing a request arriving at the left, passing through three labeled middleware boxes in sequence (logger, auth check, body parser), and finally reaching the route handler box on the right, placed right after this paragraph]

The airport security analogy

Think of your Express app like an airport. A passenger (the request) doesn't walk straight from the entrance to their gate (the route handler). They pass through a sequence of checkpoints first: ID verification, baggage scanning, boarding pass check. Each checkpoint does one specific job, and each one has the power to let you through to the next checkpoint, or stop you right there and send you back.

That's exactly what middleware does. A request comes in, passes through however many middleware functions you've set up, each one does its job, checks something, logs something, parses something, and then either calls next() to send the passenger to the next checkpoint, or ends the journey right there by sending a response, like a rejected boarding pass.

What a middleware function actually looks like

Every middleware function in Express has the same basic shape:

js

Js
function myMiddleware(req, res, next) {
  // do something with req or res
  console.log(`${req.method} request to ${req.url}`);
  next(); // pass control to the next middleware or route handler
}

app.use(myMiddleware);

Three parameters: req (the incoming request), res (the response you can send back), and next (a function you call to hand off control). That's it. That's the entire contract.

The part people forget most often is next(). If your middleware doesn't call next() and doesn't send a response either, the request just hangs there forever. The client keeps waiting, nothing times out on its own, and you'll be staring at your terminal wondering why the request never completes.

Middleware runs in order, and order matters

This is the part that trips up a lot of people building their first few Express apps. Middleware doesn't run all at once, it runs in the exact order you registered it, top to bottom in your code.

js

Js
app.use(logger);        // runs first
app.use(checkAuth);     // runs second
app.use(express.json()); // runs third
app.get("/profile", getProfile); // runs last, only if all above called next()

[IMAGE: A vertical flowchart showing the exact order above, logger then checkAuth then express.json then getProfile, with arrows labeled "next()" between each step, placed right after this code block]

If you put checkAuth before express.json(), but your auth check needed to read something from req.body, it would fail, because the body hasn't been parsed yet at that point in the chain. This is also why you'll sometimes see express.json() registered right at the top of the file, before almost anything else, it needs to run early so everything after it can rely on req.body actually being there.

Built-in and third-party middleware

Express ships with a few built-in middleware functions you've probably already used:

js

Js
app.use(express.json());              // parses JSON request bodies
app.use(express.urlencoded({ extended: true })); // parses form data
app.use(express.static("public"));    // serves static files like images and CSS

And the wider ecosystem has middleware for almost anything you'd want to do at the request level:

js

Js
const cors = require("cors");
const morgan = require("morgan");
const helmet = require("helmet");

app.use(cors());     // handles cross-origin requests, covered in detail in the CORS blog
app.use(morgan("dev")); // logs every request to the console
app.use(helmet());   // sets a bunch of security-related HTTP headers

If you've read what is CORS and how to fix it, that entire fix is middleware, cors() is just a function that inspects the request's origin and adds the right headers before passing it along, exactly the same pattern as everything else on this page.

Writing your own middleware: an auth check example

Here's a realistic one, checking if a request has a valid token before letting it reach a protected route:

js

Js
function requireAuth(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];

  if (!token) {
    return res.status(401).json({ message: "No token provided" });
  }

  try {
    const decoded = verifyToken(token); // your JWT verification logic
    req.user = decoded; // attach user info to the request for later use
    next();
  } catch (err) {
    res.status(403).json({ message: "Invalid or expired token" });
  }
}

app.get("/dashboard", requireAuth, (req, res) => {
  res.json({ message: `Welcome ${req.user.name}` });
});

Notice requireAuth is placed directly as a second argument to app.get, before the actual route handler. Express lets you chain as many middleware functions as you want for a single route this way. If requireAuth calls next(), the request moves on to the route handler. If it sends a response instead (like the 401 or 403 above), the route handler never runs at all. If you want to go deeper on what's actually inside that token and why it works this way, that's covered in how JWT authentication works.

This is also exactly why the status codes you choose here matter, a well-behaved API should be specific about why a request failed, something explained further in HTTP status codes explained.

Error-handling middleware: the one everyone forgets exists

There's a second kind of middleware most tutorials skip over: error-handling middleware. It looks almost identical to regular middleware, except it takes four parameters instead of three.

js

Js
function errorHandler(err, req, res, next) {
  console.error(err.stack);
  res.status(500).json({ message: "Something went wrong" });
}

app.use(errorHandler);

[IMAGE: A simple diagram showing a normal middleware chain on top with three parameters, and an error-handling middleware chain below it triggered by "next(err)" with four parameters, placed right after this code block]

Express recognizes this as error-handling middleware specifically because of the four arguments, that's not a style choice, it's how Express distinguishes it from regular middleware. You trigger it by calling next(err) anywhere in your app instead of next():

js

Js
app.get("/user/:id", async (req, res, next) => {
  try {
    const user = await getUserById(req.params.id);
    res.json(user);
  } catch (err) {
    next(err); // skips straight to error-handling middleware
  }
});

The important rule here: error-handling middleware needs to be registered last, after all your other routes and middleware. Express walks through middleware in order, and error handlers only catch errors from things registered before them.

Common mistakes

  • Forgetting to call next(). The request just hangs, no error, no response, nothing. This is one of the most confusing bugs to debug for the first time because nothing actually crashes.

  • Registering middleware in the wrong order. Auth checks that need req.body but run before express.json(), or route-specific middleware registered after the routes that need it.

  • Calling next() after already sending a response. This throws a "Cannot set headers after they are sent" error, usually because a middleware both calls res.send() and next() in the same code path by mistake.

  • Writing error-handling middleware with only three parameters. Express silently treats it as regular middleware instead, and it just never gets triggered by next(err).

What to actually take away from this

Middleware is just functions in a queue, each one gets a chance to look at the request, do something with it, and decide whether to pass it forward or stop it there. Once you see app.use(express.json()) as "a function that runs on every request and calls next() when it's done," the rest of Express middleware, auth checks, logging, CORS, error handling, all follow the exact same shape. You're not learning five different concepts, you're learning one pattern applied five different ways.

Comments

Sign in to join the conversation

Related Articles

More insights you might enjoy

HTTP Status Codes Explained - What They Actually Mean and Why Developers Should Know Them

HTTP Status Codes Explained - What They Actually Mean and Why Developers Should Know Them

Most developers know 200 and 404. But understanding the full range - and when to use them in your own APIs - makes you a noticeably better backend developer.

Sadie Sink·10 min
2681
Async/Await in JavaScript - How It Actually Works and Why It Exists
Backend

Async/Await in JavaScript - How It Actually Works and Why It Exists

How async/await works in JavaScript - async functions, await, error handling, parallel vs sequential execution, and the most common mistakes explained.

Sadie Sink·12 min
1651
JavaScript Closures - The Concept Interviewers Actually Test
JavaScript

JavaScript Closures - The Concept Interviewers Actually Test

Closures are one of the most asked-about JavaScript concepts in interviews, and also one of the most misunderstood. This blog explains what a closure actually is, why functions can remember variables from outside their own scope, and where closures show up in real code

Vishal Vishwakarma·9 min
1241
Higher-Order Functions in JavaScript
JavaScript

Higher-Order Functions in JavaScript

Higher-order functions are one of the most important ideas in JavaScript, and you're probably already using them without realizing it. This blog explains what makes a function "higher-order," why they matter, and how methods like map, filter, and setTimeout all rely on this same simple idea.

Vishal Vishwakarma·7 min
4530
How React's useEffect Actually Works - And Why Developers Misuse It

How React's useEffect Actually Works - And Why Developers Misuse It

useEffect is one of the most used hooks in React and also one of the most misunderstood. This guide covers the dependency array, cleanup functions, the double render, and common mistakes that cause bugs.

Sadie Sink·8 min
2860
Node.js and Express.js - What They Are, How They Differ, and How They Work Together

Node.js and Express.js - What They Are, How They Differ, and How They Work Together

Node.js and Express.js are not the same thing. This guide explains what each does, how they work together, and covers routing, middleware, and error handling with real code.

Sadie Sink·9 min
2640