Warming up the neural circuits...
By the end of this chapter you will:
Middleware is the single concept that makes Express usable. If you understand it, every "how do I X in Express" question answers itself.
A car on an assembly line passes through stations: paint, wheels, doors, electronics, QA. Each station does one thing and hands the car to the next. Any station can stop the line if something's wrong.
A middleware chain is an assembly line for a request. The req and res objects are the car. Each middleware does one thing — logs, authenticates, parses, validates — and calls next() to hand off. Any middleware can stop the line by calling res.send(...) instead of next().
A middleware is a function with three arguments:
function middleware(req, res, next) {
// do something
next();
}That's it. Three rules:
next() to pass control to the next middleware.res.send, res.json, etc.) — the chain stops.next(err) — passes to the error handler.Pick exactly one of the three. Picking zero leaves the request hanging.
You attach middleware with app.use():
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});This runs for every request, in the order it was added.
You can scope middleware to a path:
app.use('/api', authMiddleware); // only /api/*
app.use('/admin', adminOnly); // only /admin/*Or to a single route:
app.get('/profile', requireAuth, getProfile);requireAuth runs first. If it calls next(), getProfile runs.
module.exports = function logger(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const ms
Why res.on('finish') instead of logging immediately? Because at the start, you don't know the status code yet. The finish event fires when the response is fully sent.
Wire it up:
const logger = require('./middleware/logger');
app.use(logger);Now every request logs. Free observability.
const jwt = require('jsonwebtoken');
module.exports = function requireAuth(req, res, next) {
const header = req.headers.authorization;
Notice:
Authorization: Bearer ....next().req.user. The next handler reads req.user.id — clean separation.Apply it:
app.get('/profile', requireAuth, (req, res) => {
res.json({ userId: req.user.id });
});Express ships with these:
app.use(express.json({ limit: '1mb' })); // parse JSON bodies
app.use(express.urlencoded({ extended: true })); // parse form bodies
appAnd these popular ones from npm:
| Library | What it does |
|---|---|
cors | Sets headers |
helmet | Sets a dozen security headers |
morgan | Request logger (more polished than ours) |
compression | gzip the response |
cookie-parser | Parse cookies into req.cookies |
express-rate-limit | Per-IP rate limits |
A typical Express app starts with:
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
Memorize this incantation.
Middleware runs in the order you attach it. Get the order wrong and you get baffling bugs.
app.get('/profile', getProfile); // runs without auth
app.use(requireAuth); // attached too lategetProfile was registered first; the app.use(requireAuth) runs only for routes registered after it. Anyone can hit /profile.
app.use(requireAuth); // global
app.get('/profile', getProfile); // now requires authOr scope it explicitly:
app.get('/profile', requireAuth, getProfile); // per-routeMental model: middleware = a pipeline. Each step sees what previous steps did. Reorder steps, and everything downstream changes.
Express has one special middleware signature: four arguments.
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Internal error' })
The fourth argument is what tells Express this is the error handler. Express only calls it when somewhere upstream did next(err) or threw inside an wrapped handler.
Critical rule: the error handler must be the last middleware. It catches errors from everything before it.
app.use(express.json());
app.get('/users/:id', userHandler);
// ... all other routes
app.use(notFoundHandler);
app.use(errorHandler);Express 4 does not catch errors thrown from async handlers by default.
app.get('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id); // throws
If findById throws, the response is never sent. The request hangs until the client times out.
Two fixes:
Express 5 (RC at the time of writing) auto-catches async errors. If you're on 5, you're set.
Express 4 needs a wrapper:
module.exports = fn => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};Then:
const wrap = require('./utils/asyncHandler');
app.get('/users/:id', wrap(async (req, res) => {
const user = await db.users.findById(req
Any thrown error now goes to your error middleware.
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');
That's a small but production-shaped Express app. We'll add and a service layer in the next chapters.
Forgetting next() in a non-terminal middleware. The request stalls forever and your browser eventually times out with no error in the server logs.
| Mistake | Why it's wrong | What to do |
|---|---|---|
Forgetting next() | Request hangs | Always call next() or send a response |
Calling both next() and res.send() | "Cannot set headers after they are sent" | Pick one |
| Error handler before routes | Never catches their errors | Put it last |
| Async handler with no wrapper (Express 4) | Errors swallowed | Use asyncHandler |
Mutating req.body then forgetting | Surprising changes downstream | Copy, transform, or read |
Production. Add a request-ID middleware first thing. Attach the ID to every log line and forward to downstream services.
Performance. Order middleware by cost — cheapest first. A helmet() call is free; a DB lookup is not.
Security. helmet() is non-negotiable. CORS must be explicit. app.set('trust proxy', 1) only when behind a .
requireAdmin middleware that 403s when req.user.role !== 'admin'.req and to a response header.{ error, requestId } so users can quote the ID in support tickets.Beginner. What is middleware? A function with (req, res, next) that runs in the request pipeline. It can read/modify req/res, call next, or send a response.
Senior. How do you handle async errors in Express 4? Wrap async handlers with a Promise.resolve(fn).catch(next) helper.
Middleware is a function with (req, res, next). It runs in order. It can pass control, send a response, or pass an error. The standard opening — helmet, cors, morgan, json — covers 80% of every app. Order matters. The error handler is last. Async errors need a wrapper in Express 4. Master this and Express has no more surprises for you.
(req, res, next) => { ... }.next() OR send a response OR next(err).next() nor res.send()?