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.
- 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
}
Three arguments: req, res, and next. The next is what makes the chain move forward.
- 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
Every request flows through this pipeline. Middleware is the plumbing; route handlers are the destination.
- 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();
});
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);
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
d) Third-Party Middleware
Installed from npm. Examples: cors, helmet, morgan, cookie-parser.
const cors = require("cors");
app.use(cors());
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" });
});
- 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");
});
Hit / and you'll see:
1
2
3
If you put your auth middleware after your route handler, it will never protect anything. Order is everything.
- 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:
- Call
next()→ pass control to the next middleware. - Send a response (
res.send,res.json, etc.) → end the cycle. - 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();
});
- 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();
});
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 });
});
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" });
});
In production you'd reach for zod or joi, but the pattern is the same.
- 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);
Read it top to bottom. That's exactly the order a request travels.
- 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)