Part 2: Middleware
What middleware is
Middleware is a function with the signature (req, res, next). Express runs middleware functions in the order they are registered, passing control from one to the next via next(). Every route handler is itself middleware.
The chain looks like this:
HTTP request | vmiddleware 1 (logging) | vmiddleware 2 (JSON body parsing) | vmiddleware 3 (auth check) | vroute handler (res.json(...)) | vHTTP responseIf a middleware does not call next(), the chain stops there. If it does not send a response either, the client hangs.
app.use()
app.use() registers middleware. Without a path, it runs on every request:
const express = require('express');const app = express();
app.use((req, res, next) => { console.log(`${req.method} ${req.url}`); next(); // pass to the next middleware or route});
app.get('/', (req, res) => { res.send('Hello');});
app.listen(3000);With a path prefix, it only runs when the path matches:
app.use('/admin', (req, res, next) => { console.log('Admin request'); next();});Order matters
This is the single most important fact about Express middleware. Register in this order:
- Request parsing (body, cookies)
- Logging
- Security headers
- Auth
- Routes
- 404 handler
- Error handler
const express = require('express');const app = express();
// 1. Parse JSON bodiesapp.use(express.json());
// 2. Log every requestapp.use((req, res, next) => { console.log(`${new Date().toISOString()} ${req.method} ${req.url}`); next();});
// 3. Routesapp.get('/users', (req, res) => { res.json([{ id: 1, name: 'Alice' }]);});
// 4. 404 catch-all (must be after routes)app.use((req, res) => { res.status(404).json({ error: 'Not found' });});
app.listen(3000);If you put the 404 handler before the routes, every request returns 404.
Built-in middleware
Express ships with three built-in middleware functions:
express.json()
Parses incoming requests with Content-Type: application/json and populates req.body:
app.use(express.json());
app.post('/echo', (req, res) => { res.json(req.body); // whatever JSON the client sent});Without this, req.body is undefined.
Options worth knowing:
app.use(express.json({ limit: '1mb' })); // reject bodies larger than 1 MBexpress.urlencoded()
Parses HTML form submissions (Content-Type: application/x-www-form-urlencoded):
app.use(express.urlencoded({ extended: true }));extended: true uses the qs library for richer parsing (nested objects). extended: false uses the built-in querystring module.
express.static()
Serves static files from a directory:
app.use(express.static('public'));// Files in ./public/ are served at /// ./public/logo.png is available at /logo.pngmorgan: HTTP request logging
Morgan is the standard logging middleware for Express. Install it:
npm install morganconst morgan = require('morgan');
// 'dev' format: colored output, method, url, status, response timeapp.use(morgan('dev'));
// 'combined' format: Apache combined log format, good for productionapp.use(morgan('combined'));Morgan output example (dev):
GET /users 200 4.123 ms - 42POST /users 201 6.891 ms - 58GET /users/999 404 1.234 ms - 26Custom format with a token:
morgan.token('user-id', (req) => req.user?.id || 'anonymous');
app.use(morgan(':method :url :status :response-time ms - user=:user-id'));Writing custom middleware
Any function with (req, res, next) signature is middleware. Pattern for a reusable piece:
const { randomUUID } = require('crypto');
function requestId(req, res, next) { req.id = randomUUID(); res.setHeader('X-Request-Id', req.id); next();}
module.exports = requestId;const requestId = require('./middleware/requestId');
app.use(requestId);
app.get('/', (req, res) => { res.json({ requestId: req.id }); // set by middleware});This pattern: attach something to req, set a response header, call next().
next() and the three ways to call it
// 1. Pass to the next middleware or matching routenext();
// 2. Pass to the error handler (skips remaining middleware and routes)next(new Error('Something broke'));
// 3. Pass to the next route (same path, different handler) - rarenext('route');Calling next(err) skips all remaining non-error middleware and jumps to the error handler. Error handlers have 4 arguments: (err, req, res, next). Part 7 covers them in depth.
Middleware scope: app-level vs router-level
App-level middleware runs for all routes on app:
app.use(express.json());Router-level middleware runs only for routes on a specific router:
const router = express.Router();
router.use((req, res, next) => { console.log('Router-scoped middleware'); next();});
router.get('/items', (req, res) => res.json([]));
app.use('/api', router);Router-level middleware is the key to scoping auth to only protected routes.
Practical example: auth middleware
A realistic auth check that short-circuits the chain when no token is present:
function requireAuth(req, res, next) { const authHeader = req.headers['authorization']; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Missing or invalid token' }); } // Token is present; verification covered in Part 5 req.token = authHeader.slice(7); next();}
module.exports = requireAuth;Apply it selectively:
const requireAuth = require('./middleware/requireAuth');
app.get('/public', (req, res) => res.json({ ok: true }));
// requireAuth only guards this routeapp.get('/private', requireAuth, (req, res) => { res.json({ secret: 'data', token: req.token });});Or guard an entire router:
const apiRouter = express.Router();apiRouter.use(requireAuth); // all routes under /api require auth
apiRouter.get('/profile', (req, res) => res.json({ user: 'me' }));
app.use('/api', apiRouter);Gotchas
- Forgetting
next(). If your middleware doesn’t callnext()and doesn’t send a response, the request hangs until the client times out. express.json()position. It must come before any route that readsreq.body. Putting it after routes means body is alwaysundefinedfor those routes.return next(). Alwaysreturn next()when you want to stop executing the current function after passing control. Withoutreturn, code afternext()runs, which can double-callnext().- Error middleware signature. Express identifies error middleware by its 4-argument signature
(err, req, res, next). Using only 3 arguments means it’s treated as normal middleware and errors pass through it. - Morgan in tests. Morgan writes to stdout. Disable it in test environments:
if (process.env.NODE_ENV !== 'test') app.use(morgan('dev'));
What’s next
Part 3 digs into req and res in detail: every property you’ll actually use, content negotiation, and response chaining patterns.