Chapter 47: Node.js Middleware
Step 1 – What is Middleware? (the clearest explanation)
Middleware is a function that has access to the request object (req), response object (res), and the next middleware function in the application’s request-response cycle.
It sits between the incoming request and your final route handler.
Think of middleware as a chain of people passing a parcel:
- The request comes in (parcel arrives at the first person)
- Each middleware can:
- Look at the parcel (read req)
- Modify the parcel (change req.body, req.user, add headers…)
- Reject the parcel (send error response and stop)
- Pass the parcel to the next person (next())
- End the delivery (send response and stop chain)
Middleware runs in the order you write it.
Step 2 – Basic types of middleware in Express
There are four main places you can use middleware:
| Type | Syntax example | When it runs | Typical use case |
|---|---|---|---|
| Application-level | app.use(middleware) | For every request | CORS, logging, helmet, body-parser, auth globally |
| Router-level | router.use(middleware) | For routes under this router | Authentication for /api/admin/* routes |
| Route-specific | app.get(‘/users’, middleware, handler) | Only for this exact route | Validate ID before getting user |
| Error-handling | app.use((err, req, res, next) => { … }) | Only when an error occurs (4 arguments) | Global error formatting, logging |
Golden rule:
Middleware is executed sequentially — once next() is called, it moves to the next middleware. If no next() is called and no response is sent → the request hangs forever (very common bug).
Step 3 – Project setup (realistic modern Express + TypeScript)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
mkdir express-middleware-demo cd express-middleware-demo npm init -y npm pkg set type=module npm install express cors helmet compression dotenv npm install -D typescript @types/express @types/cors @types/node tsx nodemon |
tsconfig.json
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } |
package.json scripts
|
0 1 2 3 4 5 6 7 8 9 10 |
"scripts": { "dev": "tsx watch src/index.ts", "start": "node dist/index.js", "build": "tsc" } |
Step 4 – Creating our first middleware (logging middleware)
src/middleware/logger.middleware.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { Request, Response, NextFunction } from 'express' export function requestLogger(req: Request, res: Response, next: NextFunction) { const start = Date.now() console.log(`[${new Date().toISOString()}] {req.method} ${req.url}`) // Log when response finishes res.on('finish', () => { const duration = Date.now() - start console.log( `[${new Date().toISOString()}] {req.method} ${req.url} ${res.statusCode} - ${duration}ms` ) }) next() // very important — pass to next middleware } |
Using it globally
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// src/index.ts import express from 'express' import { requestLogger } from './middleware/logger.middleware.js' const app = express() app.use(requestLogger) // ← runs for EVERY request app.get('/', (req, res) => { res.json({ message: 'Hello from Express!' }) }) app.listen(3000, () => { console.log('Server running on http://localhost:3000') }) |
What you see in terminal when you visit http://localhost:3000
|
0 1 2 3 4 5 6 7 |
[2026-02-10T10:15:23.123Z] GET / [2026-02-10T10:15:23.145Z] GET / 200 - 22ms |
Step 5 – Built-in middleware (you use them every day)
Express ships with several built-in middleware functions:
|
0 1 2 3 4 5 6 7 8 |
app.use(express.json()) // parse JSON bodies app.use(express.urlencoded({ extended: true })) // parse form data app.use(express.static('public')) // serve static files (images, CSS, JS) |
Realistic usage
|
0 1 2 3 4 5 6 |
app.use('/uploads', express.static('uploads')) // serve /uploads/profile.jpg |
Step 6 – Custom middleware types – real production examples
6.1 Authentication middleware (most common use case)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// src/middleware/auth.middleware.ts import { Request, Response, NextFunction } from 'express' import jwt from 'jsonwebtoken' import { AppError } from './error.middleware.js' interface AuthPayload { userId: string role: string } declare global { namespace Express { interface Request { user?: AuthPayload } } } export function authenticate(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers.authorization if (!authHeader || !authHeader.startsWith('Bearer ')) { throw new AppError(401, 'No token provided') } const token = authHeader.split(' ')[1] try { const decoded = jwt.verify(token, process.env.JWT_SECRET!) as AuthPayload req.user = decoded next() } catch (err) { throw new AppError(401, 'Invalid token') } } |
Using it
|
0 1 2 3 4 5 6 7 |
// Protect routes app.use('/api/admin', authenticate, adminRoutes) |
6.2 Input validation middleware (using Zod)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// src/middleware/validate.middleware.ts import { Request, Response, NextFunction } from 'express' import { AnyZodObject } from 'zod' import { AppError } from './error.middleware.js' export function validateBody(schema: AnyZodObject) { return async (req: Request, res: Response, next: NextFunction) => { try { req.body = await schema.parseAsync(req.body) next() } catch (err) { next(new AppError(400, 'Validation failed')) } } } |
Usage
|
0 1 2 3 4 5 6 |
router.post('/users', validateBody(createUserSchema), userController.createUser) |
6.3 Rate limiting middleware (protect against abuse)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// src/middleware/rate-limit.middleware.ts import rateLimit from 'express-rate-limit' export const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per window message: { error: 'Too many requests, please try again later' }, standardHeaders: true, legacyHeaders: false }) // Usage app.use('/api/', apiLimiter) |
6.4 Error-handling middleware (must be last)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
app.use((err: Error, req: Request, res: Response, next: NextFunction) => { console.error(err) const status = (err as any).status || 500 const message = err.message || 'Internal Server Error' res.status(status).json({ success: false, message, ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }) }) }) |
Important order rule
Middleware is executed in the order you write it:
- Global middleware (helmet, cors, compression, logger)
- Body parsers (express.json())
- Custom middleware (auth, validation)
- Routes
- Error handler (last!)
Step 7 – Complete realistic example (small but production-like)
src/index.ts (putting it all together)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import express from 'express' import cors from 'cors' import helmet from 'helmet' import compression from 'compression' import 'dotenv/config' import env from './config/env.js' import { requestLogger } from './middleware/logger.middleware.js' import { errorHandler } from './middleware/error.middleware.js' import taskRoutes from './routes/task.routes.js' const app = express() // Security & performance app.use(helmet()) app.use(compression()) app.use(cors()) app.use(express.json()) // Logging for every request app.use(requestLogger) // Routes app.use('/api/tasks', taskRoutes) // Health check app.get('/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime() }) }) // Error handler – MUST be last app.use(errorHandler) app.listen(env.PORT, () => { console.log(`Server running → http://localhost:${env.PORT}`) }) |
Summary – Middleware in Express (2025–2026 reality)
| Middleware type | Where to place it | Typical examples | Must-have in production? |
|---|---|---|---|
| Global | app.use() (top) | helmet, cors, compression, body-parser, logger | Yes |
| Authentication | Before protected routes | JWT verify, role check | Yes (if you have auth) |
| Validation | Before controller | Zod, express-validator | Yes |
| Rate limiting | On API routes | express-rate-limit | Yes (public APIs) |
| Error handling | Last in the chain | Custom error handler | Yes (must-have) |
| Logging | Early in the chain | morgan, pino, winston | Yes |
Golden middleware order (most common real apps)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
app.use(helmet()) app.use(compression()) app.use(cors()) app.use(express.json()) app.use(requestLogger) app.use('/api/auth', authRoutes) // public routes app.use(authenticate) // protect everything below app.use('/api', apiLimiter) // rate limit protected routes app.use('/api', protectedRoutes) app.use(errorHandler) // always last |
Which part of middleware would you like to go much deeper into next?
- Full authentication middleware (JWT + role-based access)
- Rate limiting strategies (global, per-route, per-user)
- Advanced logging with Pino + correlation IDs
- Request validation with Zod + custom error messages
- Middleware performance & order optimization
- Testing middleware with Jest/Vitest
Just tell me what you want to focus on — I’ll continue with complete, production-ready code and explanations. 😊
