If you’re building with Express.js, you’ve definitely seen the term middleware thrown around. At first, it might seem like an abstract concept, but once you understand it, middleware becomes one of the most powerful parts of Express.
First, make sure to read part 1 here, where I cover the basics.
In this post, I’ll walk you through what middleware is, how to use it effectively, and some common mistakes you’ll want to avoid. By the end, you’ll know exactly how to structure your middleware so your apps stay clean, efficient, and bug-free.
What is Middleware in Express?
Middleware is just a function that runs between the time your server receives a request and when it sends back a response.
Think of it as “in-between” logic. The request comes in → middleware runs → response goes out.
In fact, even a basic Express route handler is technically middleware:
app.get("/users", (req, res) => {
res.send("Users page")
})
This handler takes in a request (req
), a response (res
), and has the potential to take in next
, which determines whether or not Express should move on to the next piece of middleware. That’s the key idea.
The Anatomy of Middleware
A middleware function typically looks like this:
function myMiddleware(req, res, next) {
// Do something with req or res
console.log("Middleware ran")
// Call next() so the next function in line runs
next()
}
The three parameters are:
-
req
→ the incoming request object -
res
→ the outgoing response object -
next
→ a function that passes control to the next middleware
If you don’t call next()
, the chain stops there.
Global Middleware with app.use
If you want middleware to run for every single request, you can use app.use
:
function logger(req, res, next) {
console.log("Request URL:", req.originalUrl)
next()
}
app.use(logger)
Now, no matter what route you hit, this logger will run first.
Important: Middleware runs in the order it’s defined. If you place your logger after your routes, it won’t run unless you explicitly call next()
inside those routes.
Route-Specific Middleware
Sometimes, you only want middleware to apply to a single route. In that case, you can pass middleware functions directly into your route definitions:
function auth(req, res, next) {
if (req.query.admin === "true") {
req.admin = true
next()
} else {
res.send("Not authorized")
}
}
app.get("/users", auth, (req, res) => {
console.log("User is admin:", req.admin)
res.send("Users page")
})
Here’s what happens:
- The
auth
middleware runs first. - If the query string includes
?admin=true
, it callsnext()
. - Otherwise, it stops the chain and sends back “Not authorized.”
Passing Data Between Middleware
Notice in the example above, we set req.admin = true
. Since each middleware shares the same req
and res
, you can attach custom properties and access them later.
app.get("/users", auth, (req, res) => {
if (req.admin) {
res.send("Welcome, Admin!")
} else {
res.send("Users page")
}
})
Common Mistake: Forgetting return
One of the most common bugs with middleware is forgetting that next()
doesn’t stop your function, it just calls the next middleware, then comes back and continues running your code.
function auth(req, res, next) {
if (req.query.admin === "true") {
req.admin = true
next()
}
res.send("Not authorized") // Still runs!
}
This causes:
Error: Cannot set headers after they are sent to the client
Fix: use return
:
function auth(req, res, next) {
if (req.query.admin === "true") {
req.admin = true
return next()
}
return res.send("Not authorized")
}
Middleware Runs Before and After next()
Code after next()
still runs, but only after the next middleware finishes.
function logger(req, res, next) {
console.log("Before next()")
next()
console.log("After next()")
}
app.get("/users", (req, res) => {
console.log("Users page")
res.send("Done")
})
Output:
Before next()
Users page
After next()
This allows you to do things before and after route handlers.
Middleware in a Nutshell
- Middleware = a function with
(req, res, next)
- Runs in order, top to bottom
- Call
next()
to continue the chain - Attach data to
req
orres
to share between middleware - Always
return
if you don’t want code to continue afternext()
- Can run before and after the next middleware
Once you internalize this, you’ll see middleware everywhere: logging, authentication, error handling, validation, body parsing, CORS, etc.
Final Thoughts
Middleware is at the heart of Express. It may look like just some functions chained together, but with the right design, you can keep your app organized, reusable, and powerful.
Next time you’re building an Express app, think about what belongs in middleware (e.g. logging, auth, validation, error handling), and your code will thank you.
Follow for practical JavaScript tutorials, web development deep-dives, and those 'aha!' moments that make coding click. New posts weekly!
Top comments (0)