Complete Node.js Middleware & Routing Tutorial
A beginner-friendly guide to understanding middleware and routing in Node.js with clear explanations and concise examples.
Middleware
Routing
Error Handling
Table of Contents
1. What is Middleware?
The Theory
Middleware is code that executes between receiving a request and sending a response. Think of it as a series of checkpoints that every request passes through.
Simple Analogy
Request → [Checkpoint 1] → [Checkpoint 2] → [Checkpoint 3] → Response
↓ ↓ ↓
Log request Check auth Parse data
Each middleware can:
- Execute any code
- Modify the request/response
- End the request-response cycle
- Call the next middleware
Why Use Middleware?
| Use Case | Example |
|---|---|
| Logging | Record every request |
| Authentication | Verify user identity |
| Parsing | Convert JSON body to object |
| Compression | Gzip responses |
| Rate limiting | Block excessive requests |
Simple Example: Basic Middleware
const http = require('http');
// Simple middleware system
class App {
constructor() {
this.middlewares = [];
this.routes = {};
}
use(fn) { this.middlewares.push(fn); return this; }
get(path, handler) { this.routes[`GET:${path}`] = handler; return this; }
async run(req, res) {
let index = 0;
const next = async () => {
if (index < this.middlewares.length) {
const middleware = this.middlewares[index++];
await middleware(req, res, next);
} else {
const handler = this.routes[`${req.method}:${req.url}`];
if (handler) handler(req, res);
else { res.writeHead(404); res.end('Not Found'); }
}
};
await next();
}
}
const app = new App();
app.use((req, res, next) => { console.log(`📝 ${req.method} ${req.url}`); next(); });
app.use((req, res, next) => { req.timestamp = new Date().toISOString(); next(); });
app.get('/', (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`<h1>Home</h1><p>Visited at: ${req.timestamp}</p>`);
});
const server = http.createServer((req, res) => app.run(req, res));
server.listen(3000, () => console.log('Server running on http://localhost:3000'));
2. How Middleware Works
The Theory
Middleware follows the Chain of Responsibility pattern. Each middleware function receives req, res, and next.
The next() Function
// How next() controls flow
function middleware1(req, res, next) {
console.log('1️⃣ Before');
next(); // Go to next middleware
console.log('1️⃣ After'); // Runs AFTER next() completes
}
function middleware2(req, res, next) {
console.log('2️⃣ Middleware');
next();
}
Simple Example: Understanding Flow
const app = new App();
app.use((req, res, next) => {
console.log('📌 Middleware 1 - Start');
req.startTime = Date.now();
next();
console.log('📌 Middleware 1 - End');
});
app.use((req, res, next) => {
console.log('📌 Middleware 2 - Processing');
console.log(` URL: ${req.url}`);
next();
});
app.use((req, res, next) => {
console.log('📌 Middleware 3 - Response');
res.writeHead(200);
res.end(`Request took ${Date.now() - req.startTime}ms`);
});
app.get('/test', (req, res) => {});
Stopping the Chain
// Authentication middleware that can stop the chain
app.use((req, res, next) => {
const token = req.headers.authorization;
if (!token) {
res.writeHead(401);
res.end('Unauthorized');
return;
}
next();
});
3. Types of Middleware
The Theory
| Type | Runs On | Example |
|---|---|---|
| Application-level | Every request | Logger, CORS |
| Router-level | Specific routes | Admin auth |
| Built-in | Framework provided | JSON parser |
| Third-party | npm packages | Compression |
| Error-handling | When errors occur | Error logger |
Simple Example: All Types
const app = new App();
// 1) Application-level
app.use((req, res, next) => { console.log(`🌍 Global: ${req.method} ${req.url}`); next(); });
app.use((req, res, next) => { req.id = Math.random().toString(36).substring(7); next(); });
// 2) Router-level style
const adminMiddleware = (req, res, next) => {
const isAdmin = req.headers['x-admin'] === 'true';
if (!isAdmin) { res.writeHead(403); res.end('Admin access required'); return; }
next();
};
// 3) Built-in style body parser
const bodyParser = (req, res, next) => {
if (req.method === 'POST') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => { try { req.body = JSON.parse(body); } catch { req.body = {}; } next(); });
} else next();
};
app.use(bodyParser);
// 4) Third-party style rate limiter
function rateLimit(max = 5, windowMs = 60000) {
const requests = new Map();
return (req, res, next) => {
const ip = req.socket.remoteAddress;
const now = Date.now();
if (!requests.has(ip)) requests.set(ip, []);
const recent = requests.get(ip).filter(t => now - t < windowMs);
if (recent.length >= max) { res.writeHead(429); res.end('Too many requests'); return; }
recent.push(now);
requests.set(ip, recent);
next();
};
}
app.use(rateLimit(3, 10000));
4. Understanding Routing
The Theory
GET /users → Show all users
GET /users/123 → Show user 123
POST /users → Create new user
PUT /users/123 → Update user 123
DELETE /users/123 → Delete user 123
Simple Router Implementation
class Router {
constructor() { this.routes = { GET: {}, POST: {}, PUT: {}, DELETE: {} }; }
get(path, handler) { this.routes.GET[path] = handler; return this; }
post(path, handler) { this.routes.POST[path] = handler; return this; }
put(path, handler) { this.routes.PUT[path] = handler; return this; }
delete(path, handler) { this.routes.DELETE[path] = handler; return this; }
handle(req, res) {
const method = req.method;
const url = req.url.split('?')[0];
const handler = this.routes[method]?.[url];
if (handler) { handler(req, res); return true; }
return false;
}
}
5. Route Parameters
The Theory
| Pattern | Example URL | Captured Values |
|---|---|---|
| /users/:id | /users/123 | { id: "123" } |
| /posts/:postId/comments/:commentId | /posts/5/comments/42 | { postId: "5", commentId: "42" } |
| /products/:category/:id | /products/electronics/789 | { category: "electronics", id: "789" } |
Simple Example: Route Parameters
class AdvancedRouter {
constructor() { this.routes = { GET: [], POST: [], PUT: [], DELETE: [] }; }
addRoute(method, pattern, handler) {
const paramNames = [];
const regexPattern = pattern.replace(/:([^\/]+)/g, (_, param) => {
paramNames.push(param);
return '([^\/]+)';
});
this.routes[method].push({ regex: new RegExp(`^${regexPattern}$`), paramNames, handler });
}
get(pattern, handler) { this.addRoute('GET', pattern, handler); return this; }
async handle(req, res) {
const url = req.url.split('?')[0];
for (const route of this.routes[req.method]) {
const match = url.match(route.regex);
if (match) {
req.params = {};
for (let i = 0; i < route.paramNames.length; i++) req.params[route.paramNames[i]] = match[i + 1];
await route.handler(req, res);
return true;
}
}
return false;
}
}
6. Error Handling Middleware
The Theory
Error handling middleware has 4 parameters: (err, req, res, next). It catches errors from middleware and routes.
Simple Example: Error Handling
class ErrorHandlingApp {
constructor() { this.middlewares = []; this.errorHandlers = []; this.routes = {}; }
use(fn) { this.middlewares.push(fn); return this; }
useError(fn) { this.errorHandlers.push(fn); return this; }
get(path, handler) { this.routes[`GET:${path}`] = handler; return this; }
async run(req, res) {
let middlewareIndex = 0;
const next = async (error) => {
if (error) { await this.handleError(error, req, res); return; }
if (middlewareIndex < this.middlewares.length) {
const middleware = this.middlewares[middlewareIndex++];
try { await middleware(req, res, next); } catch (err) { next(err); }
} else {
const handler = this.routes[`${req.method}:${req.url.split('?')[0]}`];
if (handler) { try { await handler(req, res); } catch (err) { next(err); } }
else if (!res.headersSent) { res.writeHead(404); res.end('Not Found'); }
}
};
await next();
}
async handleError(error, req, res) {
for (const handler of this.errorHandlers) await handler(error, req, res);
if (!res.headersSent) { res.writeHead(500); res.end('Internal Server Error'); }
}
}
7. Building a Complete App
Simple Example: Complete API with All Concepts
const http = require('http');
class CompleteApp {
constructor() {
this.middlewares = [];
this.errorHandlers = [];
this.routes = { GET: [], POST: [], PUT: [], DELETE: [] };
}
use(fn) { this.middlewares.push(fn); return this; }
useError(fn) { this.errorHandlers.push(fn); return this; }
addRoute(method, pattern, handler) {
const paramNames = [];
const regexPattern = pattern.replace(/:([^\/]+)/g, (_, param) => {
paramNames.push(param);
return '([^\/]+)';
});
this.routes[method].push({ regex: new RegExp(`^${regexPattern}$`), paramNames, handler });
}
get(pattern, handler) { this.addRoute('GET', pattern, handler); return this; }
post(pattern, handler) { this.addRoute('POST', pattern, handler); return this; }
put(pattern, handler) { this.addRoute('PUT', pattern, handler); return this; }
delete(pattern, handler) { this.addRoute('DELETE', pattern, handler); return this; }
async run(req, res) {
const url = new URL(req.url, `http://${req.headers.host}`);
req.query = Object.fromEntries(url.searchParams);
let middlewareIndex = 0;
let routeFound = false;
const next = async (error) => {
if (error) { await this.handleError(error, req, res); return; }
if (middlewareIndex < this.middlewares.length) {
const middleware = this.middlewares[middlewareIndex++];
try { await middleware(req, res, next); } catch (err) { next(err); }
return;
}
if (!routeFound) {
for (const route of this.routes[req.method]) {
const match = url.pathname.match(route.regex);
if (match) {
routeFound = true;
req.params = {};
for (let i = 0; i < route.paramNames.length; i++) req.params[route.paramNames[i]] = match[i + 1];
try { await route.handler(req, res); } catch (err) { next(err); }
return;
}
}
}
if (!routeFound && !res.headersSent) {
const err = new Error(`Cannot ${req.method} ${url.pathname}`);
err.status = 404;
next(err);
}
};
await next();
}
async handleError(error, req, res) {
error.status = error.status || 500;
for (const handler of this.errorHandlers) await handler(error, req, res);
if (!res.headersSent) {
res.writeHead(error.status);
res.end(JSON.stringify({ error: error.message }));
}
}
}
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
];
const app = new CompleteApp();
app.use((req, res, next) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); });
app.use((req, res, next) => {
if (req.method === 'POST' || req.method === 'PUT') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => { try { req.body = body ? JSON.parse(body) : {}; next(); } catch { next(new Error('Invalid JSON')); } });
} else next();
});
app.use((req, res, next) => {
if (req.method === 'GET' || req.url === '/') return next();
const token = req.headers.authorization;
if (!token || token !== 'Bearer secret123') {
const err = new Error('Authentication required');
err.status = 401;
next(err);
return;
}
next();
});
Quick Reference
Middleware Order (Important!)
// ALWAYS use this order
app.use(logger); // 1. Log requests
app.use(bodyParser); // 2. Parse body
app.use(auth); // 3. Authenticate
app.use(routes); // 4. Routes
app.use(errorHandler); // 5. Error handling (LAST!)
Common Middleware Patterns
| Pattern | Code |
|---|---|
| Logger | (req, res, next) => { console.log(req.url); next(); } |
| Auth | if (!token) return res.end('Unauth'); next() |
| Body Parser | req.on('data', chunk => body += chunk) |
| Rate Limit | if (count > max) return res.end('Too many') |
Route Pattern Examples
| Pattern | Matches | Captures |
|---|---|---|
| /users/:id | /users/123 | { id: "123" } |
| /posts/:postId/comments/:commentId | /posts/5/comments/42 | { postId: "5", commentId: "42" } |
| /products/:category/:id | /products/electronics/789 | { category: "electronics", id: "789" } |
HTTP Methods Quick Guide
| Method | Use | Example |
|---|---|---|
| GET | Read data | GET /users |
| POST | Create data | POST /users |
| PUT | Update all | PUT /users/1 |
| PATCH | Update partial | PATCH /users/1 |
| DELETE | Remove data | DELETE /users/1 |
Error Status Codes
| Code | Meaning | When to use |
|---|---|---|
| 400 | Bad Request | Invalid input |
| 401 | Unauthorized | No auth token |
| 403 | Forbidden | Wrong permissions |
| 404 | Not Found | Resource missing |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Server Error | Our bug |
Summary
Key Takeaways
- Middleware processes requests in sequence and can modify, stop, or forward the request.
- Routing maps URL + HTTP method to specific handlers.
- Call
next()to continue chain; send response to end chain. - Error handlers catch exceptions from middleware/routes.
- Order matters - middleware executes in registration order.
- Use route parameters (
:id) for dynamic URLs. - Always keep a final error handler for unexpected failures.
When to Use What
| Scenario | Solution |
|---|---|
| Log all requests | Application middleware |
| Protect admin routes | Route-specific middleware |
| Parse JSON body | Body parser middleware |
| Handle crashes | Error handling middleware |
| Dynamic URLs | Route parameters (:id) |
| Filter/sort data | Query parameters (?page=2) |
10 Interview Questions + 10 MCQs
Interview Pattern 10 Q&A1What is middleware?easy
Answer: A function that runs between request arrival and response send.
2Why is middleware order important?easy
Answer: Because each middleware executes in registration order and affects downstream behavior.
3What does
next() do?easyAnswer: It forwards execution to the next middleware in chain.
4How can middleware stop the chain?medium
Answer: By sending a response and not calling
next().5What are route parameters?easy
Answer: Dynamic URL segments like
/users/:id.6Difference between route params and query params?medium
Answer: Params are path-based identifiers; query params are optional filters/options.
7What is error middleware?medium
Answer: A dedicated handler that catches and formats application errors.
8Why use a body parser middleware?medium
Answer: To convert raw request body into usable JSON/object form.
9When should APIs return 429?hard
Answer: When rate limiting detects too many requests in a window.
10What is a robust middleware stack pattern?hard
Answer: Logger → parser → auth → routes → error handler.
10 Middleware & Routing MCQs
1
Middleware runs:
Explanation: Middleware is request lifecycle logic.
2What does
What does next() do?
Explanation: It continues the chain.
3
Route parameter example:
Explanation: Colon syntax defines path params.
4
Which middleware is usually last?
Explanation: Error handler should catch everything before it.
5
If auth fails, middleware should:
Explanation: Unauthorized requests should be terminated.
6
Query parameter example:
Explanation: Query strings start after
?.7
Which status for too many requests?
Explanation: Rate limiting uses 429.
8
Routing maps:
Explanation: Route resolution uses method and path.
9
Body parser middleware is mainly for:
Explanation: It converts raw payload into usable data.
10
Best place for global request logging?
Explanation: Logging all requests belongs in top-level middleware.