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?
  2. How Middleware Works
  3. Types of Middleware
  4. Understanding Routing
  5. Route Parameters
  6. Error Handling Middleware
  7. Building a Complete App
  8. Quick Reference
  9. Summary
  10. Interview Q&A + MCQ
  11. Contextual Learning Links

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 CaseExample
LoggingRecord every request
AuthenticationVerify user identity
ParsingConvert JSON body to object
CompressionGzip responses
Rate limitingBlock 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

TypeRuns OnExample
Application-levelEvery requestLogger, CORS
Router-levelSpecific routesAdmin auth
Built-inFramework providedJSON parser
Third-partynpm packagesCompression
Error-handlingWhen errors occurError 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

PatternExample URLCaptured 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

PatternCode
Logger(req, res, next) => { console.log(req.url); next(); }
Authif (!token) return res.end('Unauth'); next()
Body Parserreq.on('data', chunk => body += chunk)
Rate Limitif (count > max) return res.end('Too many')

Route Pattern Examples

PatternMatchesCaptures
/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

MethodUseExample
GETRead dataGET /users
POSTCreate dataPOST /users
PUTUpdate allPUT /users/1
PATCHUpdate partialPATCH /users/1
DELETERemove dataDELETE /users/1

Error Status Codes

CodeMeaningWhen to use
400Bad RequestInvalid input
401UnauthorizedNo auth token
403ForbiddenWrong permissions
404Not FoundResource missing
429Too Many RequestsRate limit exceeded
500Server ErrorOur 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

ScenarioSolution
Log all requestsApplication middleware
Protect admin routesRoute-specific middleware
Parse JSON bodyBody parser middleware
Handle crashesError handling middleware
Dynamic URLsRoute parameters (:id)
Filter/sort dataQuery parameters (?page=2)

10 Interview Questions + 10 MCQs

Interview Pattern 10 Q&A
1What 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?easy
Answer: 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:

ABefore app starts only
BDuring request-response lifecycle
COnly after response
DOnly in browser
Explanation: Middleware is request lifecycle logic.
2

What does next() do?

AEnds process
BMoves to next middleware
CSends 200 response
DParses JSON automatically
Explanation: It continues the chain.
3

Route parameter example:

A/users?id=1
B/users/:id
C/users#id
D/users{id}
Explanation: Colon syntax defines path params.
4

Which middleware is usually last?

ALogger
BError handler
CBody parser
DAuth
Explanation: Error handler should catch everything before it.
5

If auth fails, middleware should:

ACall next anyway
BSend 401 and stop
CSend 200
DIgnore
Explanation: Unauthorized requests should be terminated.
6

Query parameter example:

A/users/:id
B/users?page=2
C/users#page=2
D/users/page/2
Explanation: Query strings start after ?.
7

Which status for too many requests?

A404
B429
C201
D301
Explanation: Rate limiting uses 429.
8

Routing maps:

AURL + method to handler
BOnly body to DB
COnly headers to logs
DOnly errors to console
Explanation: Route resolution uses method and path.
9

Body parser middleware is mainly for:

AStatic files
BParsing request payload
CDNS resolution
DTLS cert generation
Explanation: It converts raw payload into usable data.
10

Best place for global request logging?

AApplication-level middleware
BOnly route handler
CError middleware only
DClient side
Explanation: Logging all requests belongs in top-level middleware.