What is Middleware in Express and How It Works

Full-stack developer with a good foundation in frontend, now specializing in backend development. Passionate about building efficient, scalable systems and continuously sharpening my problem-solving skills. Always learning, always evolving.
A few years back, I was debugging an Express API where a simple GET /users request was returning a 401 error. The route handler looked fine. The bug wasn’t there. It turned out three different functions were running before the request even reached the route, and one of them was silently rejecting the request. That’s when the middleware chain became very real.
Once you see it, you stop thinking in terms of “routes” and start thinking in terms of a pipeline.
What Middleware Is in Express
At its core, middleware in Express is just a function that runs between receiving a request and sending a response.
More precisely:
It has access to:
req(request)res(response)next(control function)
It can:
Execute logic
Modify
reqorresEnd the request
Pass control to the next function (expressjs.com)
That sounds abstract, but in practice:
app.use((req, res, next) => {
console.log('Incoming request');
next();
});
This function runs before your route handler. It doesn’t care which route. It intercepts everything.
The key idea:
Middleware is not “extra logic”. It is part of the request handling pipeline.
Express itself is basically a chain of middleware functions executed in sequence.
Where Middleware Sits in the Request Lifecycle
When a request hits your server, it doesn’t go straight to the route handler.
It flows through a pipeline.
Diagram: Request Flow
Client → Middleware 1 → Middleware 2 → Middleware 3 → Route Handler → Response
Step-by-step:
Request arrives at Express
First middleware runs
It either:
Ends the request
Or calls
next()
Next middleware runs
Eventually, the route handler executes
Response is sent back
Express processes middleware in sequence, one after another (GeeksforGeeks).
This pipeline model is the mental model you want. Not “Express calls my route”, but:
“The request flows through layers, and each layer decides what happens next.”
Types of Middleware
You’ll encounter a few practical categories.
1. Application-level middleware
Defined using:
app.use(...)
Runs for all routes or a subset of paths.
Use cases:
Logging
Authentication
Parsing request bodies
Example:
app.use((req, res, next) => {
console.log(req.method, req.url);
next();
});
2. Router-level middleware
Attached to a specific router.
const router = express.Router();
router.use((req, res, next) => {
console.log('Router middleware');
next();
});
Use this when:
You want isolation
You’re building modular APIs
3. Built-in middleware
Provided by Express itself.
Example:
app.use(express.json());
This parses JSON request bodies and attaches them to req.body (expressjs.com).
Other examples:
express.urlencoded()express.static()
The practical takeaway:
Middleware is how you compose behavior. Not by writing large route handlers, but by layering small functions.
Execution Order of Middleware
This is where most bugs come from.
Middleware runs in the order you define it (expressjs.com).
Example
app.use((req, res, next) => {
console.log('First');
next();
});
app.use((req, res, next) => {
console.log('Second');
next();
});
app.get('/', (req, res) => {
res.send('Done');
});
Execution order:
First → Second → Route Handler
Now the subtle bug
app.get('/', (req, res) => {
res.send(req.user);
});
app.use((req, res, next) => {
req.user = { name: 'Mohammad' };
next();
});
This will fail.
Why?
The route runs before the middleware
req.useris undefined
This is not a syntax issue. It’s a pipeline ordering problem.
If a middleware produces data, it must run before anything that consumes it.
Role of next() Function
next() is what moves the request forward.
Think of it as:
“I’m done. Pass control to the next layer.”
Basic behavior
app.use((req, res, next) => {
console.log('Step 1');
next();
});
app.use((req, res, next) => {
console.log('Step 2');
next();
});
Flow:
Step 1 → Step 2
If you don’t call next()
The request stops there.
app.use((req, res, next) => {
console.log('Blocked');
// no next()
});
Result:
Request hangs
No response is sent
Express explicitly requires that if middleware does not end the request, it must call next() (Stack Overflow).
Special behavior
You can also:
next('route');
This skips remaining middleware in the current stack and jumps to the next route.
Use it carefully. It’s easy to make control flow unpredictable.
Real-World Middleware Examples
This is where middleware actually earns its place.
1. Logging middleware
app.use((req, res, next) => {
console.log(`\({req.method} \){req.url}`);
next();
});
Purpose:
Observability
Debugging request flow
In production, you’ll use tools like morgan for structured logs (expressjs.com).
2. Authentication middleware
app.use((req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).send('Unauthorized');
}
req.user = verifyToken(token);
next();
});
Purpose:
Protect routes
Attach user context
Important detail:
- This must run before protected routes
3. Request validation middleware
app.post('/users', (req, res, next) => {
if (!req.body.email) {
return res.status(400).send('Email required');
}
next();
}, (req, res) => {
res.send('User created');
});
Purpose:
Reject bad input early
Keep route handlers clean
Diagram: Middleware Execution Chain
Request
↓
[ Logger Middleware ]
↓ (next)
[ Auth Middleware ]
↓ (next)
[ Validation Middleware ]
↓ (next)
[ Route Handler ]
↓
Response
Key observations:
Each step decides:
Continue
Or terminate
Order defines behavior
Missing
next()breaks the chain
Final Perspective
If you treat middleware as “helper functions”, you’ll eventually hit confusion.
If you treat it as a pipeline of responsibility, things become predictable:
Each middleware has a single responsibility
Order defines correctness
next()defines flow control
In real systems, most bugs around Express are not about syntax. They are about:
Incorrect ordering
Missing
next()Middleware doing too much
The clean approach:
Build small, composable middleware layers and make the request flow explicit.
That’s how you keep Express apps maintainable once they grow beyond a few routes.




