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.
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
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
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
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
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
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 CSSAnd the wider ecosystem has middleware for almost anything you'd want to do at the request level:
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 headersIf 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
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
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
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
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.bodybut run beforeexpress.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 callsres.send()andnext()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.
Related Articles
More insights you might enjoy

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.

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.

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

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.

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.

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.

Comments
Sign in to join the conversation