If you understand middleware, you understand Express. Nearly every feature of the framework, from body parsing to authentication to error handling, is implemented as middleware. This lesson demystifies the concept, shows you how to write your own, and explains the ordering rules that trip up almost every newcomer.
Middleware is simply a function that sits between the incoming request and your final route handler. It has access to the request object, the response object, and a third argument called next:
function myMiddleware(req, res, next) {
// do something with req or res
next(); // pass control to the next function
}Think of a request as travelling down a pipeline. Each middleware can inspect the request, modify it, respond immediately, or call next() to hand it further along. If a middleware neither responds nor calls next, the request hangs forever, which is the single most common middleware bug.
Register middleware for every request with app.use. A classic first example is a request logger:
const express = require('express');
const app = express();
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
next();
});
app.get('/', (req, res) => res.send('Home'));Every request now prints a timestamped line before reaching any route. Because app.use has no path here, it applies to all paths and all methods.
Express runs middleware and routes in registration order. Middleware registered after a route will never run for that route. Compare:
app.get('/first', (req, res) => res.send('No logging for me'));
app.use(logger); // registered too late for /first
app.get('/second', (req, res) => res.send('I get logged'));Rule of thumb: register general middleware (logging, parsers, static files) at the top, routes in the middle, and 404 plus error handlers at the very bottom.
Middleware often enriches the request for later handlers. A common pattern is attaching a request ID or a user object:
app.use((req, res, next) => {
req.requestTime = Date.now();
next();
});
app.get('/time', (req, res) => {
res.json({ receivedAt: req.requestTime });
});This works because the same req object flows through the whole pipeline.
You can scope middleware to a path prefix or attach it to a single route as an extra argument:
app.use('/admin', requireAuth); // everything under /admin
app.get('/dashboard', requireAuth, (req, res) => {
res.send('Secret dashboard');
});
function requireAuth(req, res, next) {
if (req.headers['x-api-key'] === 'secret123') return next();
res.status(401).json({ error: 'Unauthorized' });
}Notice how requireAuth either calls next() on success or ends the cycle with a 401. Ending early is a feature: the route handler never runs for unauthorized users.
Express ships with express.json(), express.urlencoded(), and express.static(). The npm ecosystem adds thousands more: morgan for logging, cors for cross-origin requests, helmet for security headers, and compression for gzip. Using them is one line each:
const morgan = require('morgan');
app.use(morgan('dev'));Next lesson: Serving Static Files, our first look at built-in middleware in action.