What is Middleware in Express and How It Works
What is Middleware?
In Express, middleware is a function that sits between an incoming HTTP request and the final route handler that produces a response. Every request your server receives travels through a sequence of these functions before it gets answered. Think of it as a series of checkpoints on an assembly line: each checkpoint can inspect the request, modify it, attach data to it, and either pass it along or stop it entirely.
The signature of every middleware function follows the same pattern:
function middlewareName(req, res, next) {
// do something with req or res
next(); // hand control to the next function
}
The three parameters are:
req— the incoming request objectres— the outgoing response objectnext— a callback that passes control forward
Where Middleware Sits in the Request Lifecycle
When Express receives an HTTP request, it does not jump directly to your route handler. Instead, it walks through a stack of middleware functions in the order they were registered. Each function gets a chance to inspect, transform, or terminate the request. Only after all registered middleware calls next() does the route handler execute and send the final response.
HTTP Request
|
v
Middleware 1 (Logger)
|
v
Middleware 2 (Auth)
|
v
Middleware 3 (Validation)
|
v
Route Handler
|
v
HTTP Response
If any middleware sends a response without calling next(), the pipeline stops immediately and the route handler is never reached.
Types of Middleware
1. Application-level Middleware
Registered on the app object using app.use() or app.METHOD(). It runs for every request that matches the path you specify (or all requests if no path is given).
const express = require('express');
const app = express();
// Runs on every request
app.use((req, res, next) => {
console.log(`[\({new Date().toISOString()}] \){req.method} ${req.url}`);
next();
});
// Runs only on /dashboard routes
app.use('/dashboard', (req, res, next) => {
// authentication logic
next();
});
2. Router-level Middleware
Works exactly like application-level middleware, but is bound to an instance of express.Router(). This lets you group related routes and apply middleware only to that group.
const router = express.Router();
router.use((req, res, next) => {
console.log('Router-level middleware active');
next();
});
router.get('/profile', (req, res) => {
res.send('User profile');
});
app.use('/user', router);
3. Built-in Middleware
Express ships with a small set of middleware functions you can use without installing anything extra:
| Function | Purpose |
|---|---|
express.json() |
Parses incoming JSON request bodies |
express.urlencoded() |
Parses URL-encoded form data |
express.static() |
Serves static files from a directory |
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
Execution Order of Middleware
Express runs middleware in the exact order you register it with app.use(). This matters a great deal in practice.
// Wrong order — body not parsed when validation runs
app.use(validateBody);
app.use(express.json());
// Correct order — JSON parsed before validation
app.use(express.json());
app.use(validateBody);
Middleware registered after a route will not run for that route. Always register global middleware before your route definitions.
The Role of next()
next() is the mechanism that keeps the pipeline moving. There are three ways middleware can behave:
Pass control forward — call next() with no arguments:
app.use((req, res, next) => {
req.requestTime = Date.now();
next(); // moves to the next middleware
});
Short-circuit the pipeline — send a response without calling next():
app.use((req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: 'Unauthorized' });
// next() is never called — route handler never runs
}
next();
});
Pass an error — call next(err) to skip to an error-handling middleware:
app.use((req, res, next) => {
try {
// something risky
} catch (err) {
next(err); // jumps to error handler
}
});
// Error handler has a 4th argument: err
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
Real-World Examples
Logging Middleware
Records details about every request for debugging and monitoring.
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`\({req.method} \){req.url} \({res.statusCode} — \){duration}ms`);
});
next();
});
Authentication Middleware
Verifies a JWT before allowing access to protected routes.
const jwt = require('jsonwebtoken');
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
return res.status(403).json({ error: 'Invalid token' });
}
}
// Apply only to protected routes
app.use('/api/protected', authenticate);
Request Validation Middleware
Checks that required fields are present before the route handler processes data.
function validateUserBody(req, res, next) {
const { name, email, password } = req.body;
if (!name || !email || !password) {
return res.status(400).json({
error: 'name, email, and password are required'
});
}
next();
}
app.post('/api/register', express.json(), validateUserBody, (req, res) => {
// At this point the body is guaranteed to be valid
res.status(201).json({ message: 'User registered' });
});