🏭 Express Middleware: The Assembly Line Your Requests Ride
🏭 Express Middleware: The Assembly Line Your Requests Ride
Picture a car factory. A raw chassis rolls in one end, and dozens of specialized stations each add their piece — doors here, engine there, final inspection at the end — before a finished car rolls out the other side. No single station tries to do everything. Each one has one job.
That's Express middleware in a nutshell.
Every HTTP request that hits your Express app travels through a pipeline of functions. Each function can inspect the request, modify it, respond to it, or pass it down the line. Understanding this model is the difference between writing Express apps that work and writing Express apps that you're proud of.
What Is Middleware, Actually?
A middleware function in Express has this signature:
function myMiddleware(req, res, next) {
// Do something with req or res
next(); // Pass control to the next middleware
}
That next parameter is the key. Calling next() says "I'm done here, send it down the line." Not calling it means the request stops at your function — which is fine if you've already sent a response, but catastrophic if you haven't. (Your users will sit there spinning forever. Fun for no one.)
You register middleware with app.use():
const express = require('express');
const app = express();
// This runs on EVERY request
app.use((req, res, next) => {
console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
next();
});
// This only runs on /api routes
app.use('/api', (req, res, next) => {
if (!req.headers['x-api-key']) {
return res.status(401).json({ error: 'Missing API key' });
}
next();
});
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});
Notice what happened there: the logger runs first for every request. The API key check runs second, but only for /api routes. The route handler runs last. Order matters — this is your assembly line, and you control the conveyor belt.
The Four Types of Middleware You'll Actually Use
1. Application-level middleware — attached to app, runs for matching routes. The examples above are all this type.
2. Router-level middleware — attached to express.Router(). Useful for grouping related routes with shared concerns:
const router = express.Router();
// Auth check for all routes in this router
router.use(requireAuth);
router.get('/profile', getProfile);
router.put('/profile', updateProfile);
router.delete('/account', deleteAccount);
app.use('/user', router);
Clean. Contained. Beautiful.
3. Error-handling middleware — the oddball of the family. It takes four parameters, with err first:
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message || 'Something went wrong'
});
});
Express knows this is an error handler because of that extra err argument. It only fires when you call next(err) from somewhere upstream — your escape hatch when things go sideways.
4. Built-in and third-party middleware — express.json(), express.urlencoded(), cors(), helmet(). These are just middleware functions somebody else wrote. Nothing magic about them.
Building Something Actually Useful
Let's say you want to rate-limit an endpoint without pulling in a library. Here's a simple in-memory rate limiter as middleware:
const requestCounts = new Map();
function rateLimiter(maxRequests, windowMs) {
return (req, res, next) => {
const key = req.ip;
const now = Date.now();
const windowStart = now - windowMs;
// Clean up old entries and count recent requests
const timestamps = (requestCounts.get(key) || [])
.filter(ts => ts > windowStart);
if (timestamps.length >= maxRequests) {
return res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil(windowMs / 1000)
});
}
timestamps.push(now);
requestCounts.set(key, timestamps);
next();
};
}
// Allow 10 requests per minute on the login route
app.post('/auth/login', rateLimiter(10, 60 * 1000), loginHandler);
Notice the pattern: the factory function rateLimiter(10, 60000) returns a middleware function. This is idiomatic Express — you configure the middleware with parameters, and it returns a closure with that configuration baked in. It's how cors({ origin: 'example.com' }) and express.json({ limit: '10mb' }) work under the hood.
The Gotchas That Will Bite You
Forgetting to call next() — Your route handler fires and... nothing. The browser waits. You panic. Always either call next(), next(err), or send a response. Every code path.
Order is everything — express.json() must come before any route that reads req.body. Your auth middleware must come before protected routes. Your error handler must come last, after all routes. Mount things in the wrong order and you'll spend an afternoon debugging something that has nothing to do with your actual feature.
next('route') exists — Calling next('route') skips remaining handlers for the current route and moves to the next matching route definition. Rarely needed, but when you need it, nothing else will do.
Async middleware needs a try/catch — Express 4 doesn't catch async errors automatically. Either wrap in try/catch and call next(err), or use a wrapper like express-async-errors. Express 5 fixes this, but many codebases are still on 4.
Why This Matters
Middleware is how you separate concerns without turning your codebase into a bowl of spaghetti. Auth logic doesn't belong in your route handlers. Logging doesn't belong inside your business logic. Request validation shouldn't be copy-pasted across 40 endpoints.
When you understand the middleware pipeline, you start seeing your Express app differently — not as a collection of route handlers, but as a composed system where each layer has exactly one responsibility.
That's the factory model. Clean, composable, and honestly kind of satisfying once it clicks.
Want to go deeper? Try refactoring one of your existing Express apps to pull out repeated logic (auth checks, input validation, logging) into dedicated middleware files. You'll be surprised how much cleaner the route handlers get when they only have to worry about the actual business logic.
What's your favorite piece of middleware you've built or discovered? Drop it in the comments — I'm always looking for new tricks.