What is Middleware in Express and How It Works

1. What middleware is in Express
Introduction
Every web application needs to handle more than just incoming requests and outgoing responses. Between those two points lies a critical layer — one that handles logging, authentication, error checking, and data transformation. In Express, that layer is called middleware.
Understanding middleware is the key to writing clean, modular, and maintainable Express applications. Once the concept clicks, you'll see it everywhere.
Definition
Middleware in Express is any function that has access to the request object (req), the response object (res), and a third argument called next. It sits between the incoming request and the final route handler, acting as a checkpoint in the pipeline.
The function signature looks like this:
function myMiddleware(req, res, next) {
// do something
next(); // pass control to the next step
}
If next() is called, the request moves forward. If it isn't — say, the middleware sends a response directly — the chain stops. That's the short-circuit.
Explanation
Think of an Express application as an assembly line. A request enters at one end, travels through a series of stations (middleware), and eventually reaches the route handler. Each station can inspect, modify, or reject the request before passing it along.
The key insight is that middleware is ordered. Express runs each function in the sequence it was registered. The first diagram shows the simplest possible pipeline — one middleware standing between the client and the route handler.
In real applications, you rarely have just one middleware. Multiple functions stack in sequence, each calling next() to hand off to the one after it. This is the middleware chain — and it's what gives Express its composable, pluggable architecture.
If the Auth check middleware finds an invalid token, it calls res.send() instead of next(), and the request never reaches the route handler or anything below it.
Examples
A basic logging middleware:
function logger(req, res, next) {
console.log(`\({req.method} \){req.url}`);
next(); // pass to the next middleware or route
}
An auth middleware that short-circuits on failure:
function authCheck(req, res, next) {
const token = req.headers['authorization'];
if (!token) {
return res.status(401).json({ error: 'Unauthorized' }); // stops here
}
next(); // token present — continue
}
Registering middleware in an Express app:
const express = require('express');
const app = express();
app.use(logger); // runs on every request
app.use(express.json()); // built-in: parses JSON bodies
app.use(authCheck); // runs after logger and json parser
app.get('/dashboard', (req, res) => {
res.send('Welcome!'); // only reached if authCheck calls next()
});
Middleware registered with app.use() runs globally. You can also scope it to specific routes by passing it directly into app.get(), app.post(), etc.
Conclusion
Middleware is what makes Express flexible. Rather than cramming authentication, logging, parsing, and error handling into every route handler, you define each concern once and compose them in order. The pipeline model — where each function either calls next() or short-circuits with a response — keeps your application modular and easy to reason about.
Once you've internalized the request → middleware chain → route handler flow, building and debugging Express applications becomes significantly more intuitive.
2. Where middleware sits in request lifecycle
Introduction
Every web request goes on a journey. It leaves the client, travels to your server, gets processed, and comes back as a response. That journey isn't a straight line — it passes through a series of checkpoints before anything useful happens. Those checkpoints are middleware.
Definition
Middleware is a function that sits between an incoming request and the final route handler. It receives the request, can inspect or modify it, and then either passes it along to the next function or stops the cycle entirely by sending a response early.
Think of it like airport security. You board a plane (make a request), but before you reach your seat (route handler), you pass through check-in, security screening, and boarding — each step examining and passing you through. Middleware works the same way
Explanation
Here's where middleware physically lives in the cycle:
The key idea is that the request passes through middleware — middleware isn't the destination. It can read the request, attach data to it, log it, block it, or simply call next() to hand it off. The route handler only runs if every middleware in the chain says so.
When multiple middleware functions are registered, they execute in order — one after another, like a pipeline. Each one calls next() to advance the chain.
Notice that each middleware calls next() to hand off control. If any one of them doesn't — say, Auth finds an invalid token — it sends a response immediately and the chain stops. The route handler never runs.
Examples
A simple middleware in Express looks like this:
function logger(req, res, next) {
console.log(`\({req.method} \){req.path}`);
next(); // hand off to the next function
}
And the chain is registered like so:
app.use(logger); // runs 1st
app.use(authenticate); // runs 2nd
app.use(rateLimit); // runs 3rd
app.get('/dashboard', (req, res) => {
res.send('Welcome!'); // only reached if all three pass
});
A middleware that blocks the chain — for example, an auth check — simply responds early without calling next():
function authenticate(req, res, next) {
if (!req.headers.authorization) {
return res.status(401).send('Unauthorized'); // chain stops here
}
next();
}
Conclusion
Middleware is where cross-cutting concerns live — logging, authentication, rate limiting, CORS, compression. By placing them between the raw request and your business logic, you keep route handlers clean and focused. The request pipeline is predictable: middleware runs in registration order, each one deciding whether to pass control forward or end the cycle early. That simplicity is what makes middleware one of the most powerful patterns in server-side development.
3. Types of middleware:
Application-level middleware
Router-level middleware
Built-in middleware
Introduction
In Express.js, every HTTP request travels through a pipeline before reaching its destination. Middleware functions are the checkpoints in that pipeline — each one can inspect, modify, or terminate the request-response cycle. Understanding middleware is fundamental to building well-structured Express applications.
Definition
Middleware is any function with the signature (req, res, next) that sits between an incoming request and the final route handler. It has access to the request object, the response object, and the next() function, which passes control to the next checkpoint in the chain.
Explanation & Description
Think of middleware as a conveyor belt of inspectors. A request enters at one end and must pass through each inspector in order. Each inspector can:
Read or modify the request (e.g. parse the body, verify a token)
Send a response early and stop the chain (e.g. reject unauthorized users)
Call
next()to pass control downstream
Express supports three main types of middleware — let's look at each one, then trace how they chain together.
Now let's trace exactly what happens when a request enters the pipeline and travels through each checkpoint in sequence.
Example
Application-level middleware — runs on every request, globally bound to the app object:
const app = express();
app.use((req, res, next) => { console.log([\({new Date().toISOString()}] \){req.method} ${req.url}); next(); // pass control to the next middleware });
const app = express();
app.use((req, res, next) => {
console.log(`[\({new Date().toISOString()}] \){req.method} ${req.url}`);
next(); // pass control to the next middleware
});
Router-level middleware — scoped to a specific router, keeping features modular:
const router = express.Router();
router.use((req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
router.get('/profile', (req, res) => res.send('Your profile'));
app.use('/api/users', router);
Built-in middleware — provided by Express, handles common parsing tasks:
app.use(express.json()); // parses JSON request bodies
app.use(express.urlencoded({ extended: true })); // parses URL-encoded forms
app.use(express.static('public')); // serves static files from /public
Conclusion
Middleware is Express's superpower. By treating every request as a pipeline, you can compose independent, reusable checkpoints — built-in parsers handle raw data, application-level middleware enforces global concerns like logging and auth, and router-level middleware keeps feature-specific logic neatly scoped. The key rule: always call next() unless you intentionally end the request-response cycle with a response. Master that, and middleware becomes second nature.
4. Execution order of middleware
The golden rule: middleware executes in the order it is defined.
The three rules of execution order
Rule 1 — Registration order is execution order. Express runs middleware top-to-bottom, exactly as you wrote it. If you register express.json() after your route handler, the body won't be parsed when the handler runs.
// Correct — parser runs before the handler sees req.body
app.use(express.json());
app.post('/data', (req, res) => res.json(req.body));
// Wrong — req.body is undefined inside the handler
app.post('/data', (req, res) => res.json(req.body));
app.use(express.json());
Rule 2 — next() is the gate. A middleware that doesn't call next() (and doesn't send a response) silently freezes the request. Always either call next() or end the cycle with res.send() / res.json() / etc.
app.use((req, res, next) => {
if (!req.headers['x-api-key']) {
return res.status(401).json({ error: 'Missing API key' }); // ← ends here
}
next(); // ← continues the chain
});
Rule 3 — Router-level middleware runs only after reaching its router. If you mount a router at /api/users, its router.use() calls only fire for requests that match that prefix. Global app.use() runs first, for every request.
app.use(express.json()); // runs for ALL routes
app.use(logger); // runs for ALL routes
const userRouter = express.Router();
userRouter.use(authGuard); // runs ONLY for /api/users/* requests
userRouter.get('/', handler);
app.use('/api/users', userRouter);
Common ordering mistake
A very frequent bug is registering a catch-all or error handler too early:
// This catches everything — your real routes never fire
app.use((req, res) => res.status(404).send('Not found'));
app.get('/users', handler); // ← never reached
// 404 handler goes last, after all routes
app.get('/users', handler);
app.use((req, res) => res.status(404).send('Not found'));
Key takeaway
The execution order in Express is purely sequential and deterministic — no magic, no priority system. The order you write app.use() and router.use() calls in your code is exactly the order requests flow through them. Design your pipeline from most general (parsers, loggers) to most specific (route guards, handlers), and always put error and 404 handlers last.
5. Role of next() function
next() is the baton in the middleware relay race. When a middleware finishes its job, it calls next() to hand control to whatever comes next in the pipeline — the next middleware, or the route handler. Without it, the request simply stalls.
There are three distinct things next() can do, depending on how it's called.
The three ways to call next()
next() — continue the chain
The most common use. Passes control to the immediately next registered middleware or route handler. The current middleware's job is done.
app.use((req, res, next) => {
console.log('Request received:', req.method, req.url);
next(); // hand off to whatever is registered next
});
res.send() / res.json() — end the cycle (no next())
If middleware sends a response itself, it must not call next(). The cycle is complete. Calling next() after res.send() causes a "headers already sent" error.
app.use((req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'Unauthorized' }); // ends here — no next()
}
next(); // only reached if token exists
});
next(err) — skip to the error handler
Passing any value into next() tells Express something went wrong. It skips all remaining regular middleware and routes, jumping directly to the nearest error-handling middleware — which is identified by its four-parameter signature (err, req, res, next).
app.use((req, res, next) => {
try {
const data = JSON.parse(req.body.payload);
req.parsed = data;
next();
} catch (err) {
next(err); // skip straight to the error handler
}
});
// Error handler — must have exactly 4 parameters
app.use((err, req, res, next) => {
console.error(err.message);
res.status(500).json({ error: 'Something went wrong' });
});
The frozen request — a common mistake
If a middleware neither calls next() nor sends a response, the request hangs indefinitely. The client times out and gets no reply. This is one of the most frequent bugs in Express apps.
// Request freezes — client never gets a response
app.use((req, res, next) => {
if (req.headers['x-custom-header']) {
req.customData = 'set';
// forgot to call next() !
}
next(); // only called if the header is missing — wrong
});
// Always call next() on every code path
app.use((req, res, next) => {
if (req.headers['x-custom-header']) {
req.customData = 'set';
}
next(); // called regardless
});
Key takeaway
Think of next() as the green light at a checkpoint. Every middleware must make a clear decision: call next() to wave the request through, call next(err) to escalate a problem, or send a response to close the gate. There is no fourth option — indecision stalls the pipeline permanently.
6. Real-world examples:
Logging
Authentication
Request validation
Real-world Middleware Examples
These three patterns appear in virtually every production Express app. Each one is a self-contained checkpoint with a single responsibility.
1. Logging middleware
Logging never blocks a request — it observes silently and always calls next(). It's registered first so it captures every request before anything else can short-circuit the chain.
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`[\({new Date().toISOString()}] \){req.method} \({req.url} \){res.statusCode} — ${duration}ms`);
});
next(); // always continues — logging never blocks
});
The res.on('finish') trick logs the status code after the response is sent, giving you the full picture — method, URL, status, and duration — in one line.
2. Authentication middleware
Auth is a gatekeeper. It either verifies the token and enriches req with user data, or it short-circuits the chain with a 401 before the request gets anywhere near your business logic.
const jwt = require('jsonwebtoken');
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or malformed token' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // attach user to req for downstream use
next(); // token valid — continue the chain
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
};
// Apply only to protected routes
app.use('/api/protected', authenticate);
Notice that req.user = decoded is the handshake between auth and the route handler — downstream code can trust req.user exists without re-verifying
3. Request validation middleware
Validation ensures the route handler only ever receives clean, well-shaped data. It rejects malformed payloads early with a 400, so your business logic never has to guard against missing or invalid fields.
const validateCreateUser = (req, res, next) => {
const { name, email, password } = req.body;
const errors = [];
if (!name || typeof name !== 'string') {
errors.push('name is required and must be a string');
}
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.push('a valid email is required');
}
if (!password || password.length < 8) {
errors.push('password must be at least 8 characters');
}
if (errors.length > 0) {
return res.status(400).json({ errors }); // fails fast — no next()
}
next(); // all fields valid — proceed to handler
};
app.post('/api/users', authenticate, validateCreateUser, (req, res) => {
// req.body is guaranteed clean here
res.status(201).json({ message: 'User created', user: req.body.name });
});
Putting all three together
const express = require('express');
const app = express();
app.use(express.json());
app.use(logger); // 1. Log every request
app.post(
'/api/users',
authenticate, // 2. Verify token
validateCreateUser, // 3. Check body shape
(req, res) => { // 4. Business logic — safe zone
res.status(201).json({ message: 'User created' });
}
);
Each middleware has exactly one job. The logger observes, auth verifies identity, validation checks shape — and the route handler only runs when all three checkpoints have passed.





