Middleware — Patterns & Best Practices

📖 Concept

Middleware is the core architectural pattern of Express.js. A middleware function has access to the request (req), response (res), and the next function — which passes control to the next middleware in the stack.

Middleware signature:

function middleware(req, res, next) {
  // Do something with req/res
  next(); // Pass to next middleware
}

Types of middleware:

  1. Application-levelapp.use(fn) — runs for every request
  2. Route-levelapp.get('/path', fn, handler) — runs for specific routes
  3. Router-levelrouter.use(fn) — runs for routes in that router
  4. Built-inexpress.json(), express.static(), express.urlencoded()
  5. Third-partycors(), helmet(), morgan(), compression()
  6. Error-handling(err, req, res, next) — 4-parameter signature

Middleware execution order:

Request → middleware1 → middleware2 → route handler → Response
                                        ↓ (if error)
                                    error middleware

Common middleware stack (production):

app.use(helmet())                    // Security headers
app.use(cors())                      // CORS
app.use(compression())               // Gzip compression
app.use(morgan('combined'))          // Request logging
app.use(express.json())              // JSON body parsing
app.use(express.urlencoded(...))     // Form body parsing
app.use(rateLimit(...))              // Rate limiting
app.use('/api', authMiddleware)      // Authentication
app.use('/api', routes)              // Application routes
app.use(notFoundHandler)             // 404 handler
app.use(errorHandler)                // Error handler

🏠 Real-world analogy: Middleware is like an airport security process. Each checkpoint (middleware) inspects your luggage (request), adds a stamp (modifies req), and either lets you pass (next()) or stops you (res.status(403)). The order matters — you go through identity check (auth), then baggage scan (validation), then boarding (route handler).

💻 Code Example

codeTap to expand ⛶
1// Middleware — Patterns & Best Practices
2
3const express = require("express");
4const app = express();
5
6// 1. Request logging middleware
7function requestLogger(req, res, next) {
8 const start = Date.now();
9
10 // Capture the original res.end to measure response time
11 const originalEnd = res.end;
12 res.end = function (...args) {
13 const duration = Date.now() - start;
14 console.log(
15 `[${new Date().toISOString()}] ${req.method} ${req.originalUrl}${res.statusCode} (${duration}ms)`
16 );
17 originalEnd.apply(this, args);
18 };
19
20 next();
21}
22
23// 2. Authentication middleware
24function authenticate(req, res, next) {
25 const token = req.headers.authorization?.replace("Bearer ", "");
26
27 if (!token) {
28 return res.status(401).json({ error: "Authentication required" });
29 }
30
31 try {
32 // In production: verify JWT token
33 // const decoded = jwt.verify(token, process.env.JWT_SECRET);
34 const decoded = { id: 1, role: "admin" }; // Mock
35 req.user = decoded; // Attach user to request
36 next();
37 } catch (err) {
38 res.status(401).json({ error: "Invalid token" });
39 }
40}
41
42// 3. Authorization middleware (factory function)
43function authorize(...roles) {
44 return (req, res, next) => {
45 if (!req.user) {
46 return res.status(401).json({ error: "Not authenticated" });
47 }
48 if (!roles.includes(req.user.role)) {
49 return res.status(403).json({
50 error: `Requires one of roles: ${roles.join(", ")}`,
51 });
52 }
53 next();
54 };
55}
56
57// 4. Request validation middleware (factory)
58function validate(schema) {
59 return (req, res, next) => {
60 const errors = [];
61
62 for (const [field, rules] of Object.entries(schema)) {
63 const value = req.body[field];
64
65 if (rules.required && (value === undefined || value === "")) {
66 errors.push(`${field} is required`);
67 }
68 if (rules.type && value !== undefined && typeof value !== rules.type) {
69 errors.push(`${field} must be a ${rules.type}`);
70 }
71 if (rules.minLength && value && value.length < rules.minLength) {
72 errors.push(`${field} must be at least ${rules.minLength} characters`);
73 }
74 if (rules.pattern && value && !rules.pattern.test(value)) {
75 errors.push(`${field} format is invalid`);
76 }
77 }
78
79 if (errors.length > 0) {
80 return res.status(400).json({ error: "Validation failed", details: errors });
81 }
82 next();
83 };
84}
85
86// 5. Rate limiting middleware
87function rateLimit({ windowMs = 60000, maxRequests = 100 } = {}) {
88 const clients = new Map();
89
90 // Cleanup old entries periodically
91 setInterval(() => {
92 const now = Date.now();
93 for (const [key, data] of clients) {
94 if (now - data.startTime > windowMs) {
95 clients.delete(key);
96 }
97 }
98 }, windowMs);
99
100 return (req, res, next) => {
101 const clientIP = req.ip || req.connection.remoteAddress;
102 const now = Date.now();
103
104 if (!clients.has(clientIP)) {
105 clients.set(clientIP, { count: 1, startTime: now });
106 return next();
107 }
108
109 const client = clients.get(clientIP);
110
111 if (now - client.startTime > windowMs) {
112 clients.set(clientIP, { count: 1, startTime: now });
113 return next();
114 }
115
116 client.count++;
117
118 if (client.count > maxRequests) {
119 res.setHeader("Retry-After", Math.ceil((windowMs - (now - client.startTime)) / 1000));
120 return res.status(429).json({ error: "Too many requests" });
121 }
122
123 next();
124 };
125}
126
127// 6. Error handling middleware (MUST have 4 parameters)
128function errorHandler(err, req, res, next) {
129 console.error("Error:", {
130 message: err.message,
131 stack: process.env.NODE_ENV === "development" ? err.stack : undefined,
132 path: req.path,
133 method: req.method,
134 });
135
136 const statusCode = err.statusCode || 500;
137 res.status(statusCode).json({
138 error: err.isOperational ? err.message : "Internal server error",
139 ...(process.env.NODE_ENV === "development" && { stack: err.stack }),
140 });
141}
142
143// === Apply middleware ===
144app.use(requestLogger);
145app.use(express.json());
146
147// Public routes
148app.get("/api/health", (req, res) => {
149 res.json({ status: "ok" });
150});
151
152// Protected routes
153app.use("/api/admin", authenticate, authorize("admin"));
154
155app.get("/api/admin/dashboard", (req, res) => {
156 res.json({ message: "Admin dashboard", user: req.user });
157});
158
159// Route with validation
160app.post(
161 "/api/users",
162 authenticate,
163 validate({
164 name: { required: true, type: "string", minLength: 2 },
165 email: { required: true, type: "string", pattern: /^[^@]+@[^@]+\.[^@]+$/ },
166 }),
167 (req, res) => {
168 res.status(201).json({ data: req.body });
169 }
170);
171
172// Error handler (must be LAST)
173app.use(errorHandler);
174
175app.listen(3000);

🏋️ Practice Exercise

Exercises:

  1. Write a request logging middleware that logs method, URL, status code, and response time
  2. Build an authentication middleware that verifies JWT tokens and attaches the user to req.user
  3. Create a role-based authorization middleware factory: authorize('admin', 'moderator')
  4. Implement a rate limiter using a Map-based sliding window counter
  5. Build a request validation middleware factory that validates req.body against a schema
  6. Chain multiple middleware on a single route: authenticate → authorize → validate → handler

⚠️ Common Mistakes

  • Forgetting to call next() — the request hangs indefinitely because Express doesn't automatically pass to the next handler

  • Putting error-handling middleware before routes — Express processes middleware in order; error handlers must come last

  • Using 3-parameter functions for error handling — error handlers MUST have exactly 4 parameters: (err, req, res, next)

  • Not returning after sending a response in middleware — without return, the code continues and may call next() or send another response

  • Adding middleware after app.listen() — middleware must be registered before the server starts accepting requests

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Middleware — Patterns & Best Practices. Login to unlock this feature.