Middleware is the single most important concept in Express. It is the mechanism behind JSON parsing, authentication, logging, CORS, rate limiting, error handling โ everything that happens to a request between the moment it arrives and the moment a response is sent. Understanding middleware deeply means you can read any Express codebase, debug request problems quickly, and compose your own reusable request processing logic. In this lesson you will learn exactly what a middleware function is, how the pipeline works, and why the order in which you register middleware is critical.
What Is Middleware?
A middleware function is any function with access to the request object (req), the response object (res), and the next function. When called, next() passes control to the next middleware in the pipeline. When not called, the pipeline stops โ the request is either responded to or left hanging.
// The middleware function signature
function myMiddleware(req, res, next) {
// 1. Do something with req (read headers, parse body, verify token...)
// 2. Optionally modify req or res (attach user, set headers...)
// 3. Either:
// a. Call next() to pass to the next middleware / route handler
// b. Call res.json() / res.send() to end the request-response cycle
// c. Call next(err) to jump to the error handling middleware
next();
}
The Request Pipeline โ Visualised
Incoming HTTP Request
โ
โผ
โโโโโโโโโโโโโโโโโโโโโ
โ helmet() โ โ sets security headers on res
โ next() โ
โโโโโโโโโโโฌโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโ
โ cors() โ โ adds Access-Control-Allow-Origin header
โ next() โ
โโโโโโโโโโโฌโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโ
โ morgan() โ โ logs: GET /api/posts 200 12ms
โ next() โ
โโโโโโโโโโโฌโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโ
โ express.json() โ โ parses JSON body โ populates req.body
โ next() โ
โโโโโโโโโโโฌโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโ
โ protect() โ โ verifies JWT โ attaches req.user
โ next() or 401 โ โ if no token: res.status(401).json(...)
โโโโโโโโโโโฌโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโ
โ Route Handler โ โ queries MongoDB, sends JSON response
โ res.json(data) โ
โโโโโโโโโโโโโโโโโโโโโ
โ
โผ
HTTP Response sent to client
app.use() or app.get/post/.... If express.json() is registered after your routes, those routes will have an empty req.body. If cors() is registered after a route that handles OPTIONS preflight, the browser’s CORS check will fail. Order is everything.next()), send it off early (res.json()), or report a problem (next(err)). Stations registered earlier always run before stations registered later โ you design the belt by choosing what you register and in what order.next() nor sends a response, the request will hang indefinitely until the client times out. This is one of the hardest bugs to spot โ the server receives the request, logs it, but the client never gets a response. Always make sure every code path in your middleware either calls next() or sends a response.Three Types of Middleware in Express
| Type | Scope | Registration | Example |
|---|---|---|---|
| Application-level | All routes on the app | app.use(fn) |
cors, helmet, morgan, express.json() |
| Router-level | All routes on a Router | router.use(fn) |
Auth guard for an entire resource group |
| Route-level | One specific route | app.get('/path', fn, handler) |
Validate ObjectId only on parameterised routes |
Application-Level Middleware
const express = require('express');
const app = express();
// Runs for EVERY request to the app, regardless of path or method
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
next();
});
// Runs for EVERY request to paths starting with /api
app.use('/api', (req, res, next) => {
res.set('X-API-Version', '1.0');
next();
});
// Routes are also middleware โ they just end the pipeline when matched
app.get('/api/posts', (req, res) => {
res.json({ data: [] });
});
Router-Level Middleware
// routes/posts.js โ auth middleware applies to ALL post routes
const express = require('express');
const router = express.Router();
const protect = require('../middleware/auth');
// Apply protect middleware to ALL routes in this router
router.use(protect);
// Every route below this line requires authentication
router.get('/', getAllPosts);
router.post('/', createPost);
router.get('/:id', getPostById);
router.put('/:id', updatePost);
Route-Level Middleware
// Middleware applies only to the routes where it is explicitly added
const protect = require('../middleware/auth');
const validateObjectId = require('../middleware/validateObjectId');
const checkOwnership = require('../middleware/checkOwnership');
// Public โ no middleware
router.get('/', getAllPosts);
router.get('/:id', validateObjectId('id'), getPostById); // validate ID only
// Protected โ auth required
router.post('/', protect, createPost);
// Protected + ownership check
router.put('/:id', protect, validateObjectId('id'), checkOwnership, updatePost);
router.delete('/:id', protect, validateObjectId('id'), checkOwnership, deletePost);
How next() Works
// next() โ pass to next middleware/route
const logger = (req, res, next) => {
console.log('Request received');
next(); // โ continue pipeline
};
// next(err) โ skip to error middleware
const protect = (req, res, next) => {
if (!req.headers.authorization) {
return next(new Error('No token')); // โ jump to error handler
}
next(); // โ all good, continue
};
// next('router') โ skip remaining middleware in current Router
// next('route') โ skip remaining handlers for current route
// (advanced โ rarely needed in standard MERN APIs)
Common Mistakes
Mistake 1 โ Registering middleware after routes that need it
โ Wrong โ express.json() after the POST route means req.body is undefined:
app.post('/api/posts', (req, res) => {
console.log(req.body); // undefined!
});
app.use(express.json()); // too late โ registered after the route
โ Correct โ middleware must be registered before the routes that depend on it:
app.use(express.json()); // always before routes
app.post('/api/posts', handler); // req.body is now populated โ
Mistake 2 โ Calling next() after sending a response
โ Wrong โ calling both res.json() and next() causes a “headers already sent” error:
app.use((req, res, next) => {
res.json({ message: 'intercepted' }); // sends response
next(); // then tries to continue pipeline โ ERROR
});
โ Correct โ once a response is sent, stop pipeline execution with return:
app.use((req, res, next) => {
if (someCondition) return res.json({ message: 'intercepted' }); // stops here
next(); // only reaches here if condition was false
});
Mistake 3 โ Forgetting next() in a middleware that should pass through
โ Wrong โ middleware that should log and pass on, but doesn’t:
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
// forgot next() โ all requests hang here
});
โ Correct:
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next(); // โ
});
Quick Reference
| Task | Code |
|---|---|
| Apply to all routes | app.use(middlewareFn) |
| Apply to path prefix | app.use('/api', middlewareFn) |
| Apply to router routes | router.use(middlewareFn) |
| Apply to single route | app.get('/path', mw, handler) |
| Pass to next middleware | next() |
| Pass to error handler | next(new Error('msg')) |
| End pipeline with response | return res.status(401).json({...}) |