Express.js Middleware and Routing: A Developer's Guide

A comprehensive guide for understanding Express.js middleware and routing concepts

Introduction to Express.js Architecture

Think of Express.js as a sophisticated restaurant kitchen. Just as a restaurant processes orders through various stations (prep, cooking, plating, etc.), Express.js processes HTTP requests through different layers of middleware before serving the final response. This system allows for modular, organized, and efficient handling of web requests.

Understanding Middleware

Middleware functions are like quality control stations in a manufacturing line. Each piece of middleware can:

Basic Middleware Structure


// Basic middleware function
const simpleLogger = (req, res, next) => {
    console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
    next(); // Pass control to next middleware
};

// Using the middleware
app.use(simpleLogger);
            

Middleware Execution Order

Middleware execution in Express follows a "waterfall" pattern, similar to water flowing through a series of filters. Here's a visual example:


app.use((req, res, next) => {
    console.log('First middleware - Always runs');
    next();
});

app.get('/api/*', (req, res, next) => {
    console.log('API route middleware - Only runs for /api routes');
    next();
});

app.get('/api/users', (req, res) => {
    console.log('Route handler - Final destination');
    res.send('User data');
});
            

Express Router vs Application

Think of an Express application as a large office building, and Express routers as different departments within that building. Each department (router) handles specific types of requests, making the overall system more organized and maintainable.

Creating a Router


// userRouter.js
const express = require('express');
const router = express.Router();

// Department-specific middleware
router.use((req, res, next) => {
    console.log('User department middleware');
    next();
});

// Handle user-related routes
router.get('/', (req, res) => {
    res.send('List of users');
});

router.post('/', (req, res) => {
    res.send('Create new user');
});

module.exports = router;

// app.js
const userRouter = require('./userRouter');
app.use('/users', userRouter);
            

RESTful Routing with Express Router

RESTful routing is like creating a standardized filing system for your web application. Here's a complete example of a product router implementing RESTful conventions:


// productRouter.js
const express = require('express');
const router = express.Router();

// Middleware specific to product routes
const validateProduct = (req, res, next) => {
    if (!req.body.name || !req.body.price) {
        return res.status(400).json({ error: 'Name and price required' });
    }
    next();
};

// GET /products - List all products
router.get('/', (req, res) => {
    res.json([/* product list */]);
});

// GET /products/:id - Get single product
router.get('/:id', (req, res) => {
    res.json({ id: req.params.id, name: 'Sample Product' });
});

// POST /products - Create product
router.post('/', validateProduct, (req, res) => {
    res.status(201).json({ message: 'Product created' });
});

// PUT /products/:id - Update product
router.put('/:id', validateProduct, (req, res) => {
    res.json({ message: 'Product updated' });
});

// DELETE /products/:id - Delete product
router.delete('/:id', (req, res) => {
    res.json({ message: 'Product deleted' });
});

module.exports = router;
            

Error Handling Middleware

Error handling middleware functions like a safety net in a circus - they catch errors that occur anywhere in your application. They're distinguished by having four parameters instead of three:


// Regular middleware
app.use((req, res, next) => {
    // Normal processing
    next();
});

// Error handling middleware
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Something broke!');
});

// Example of error throwing and handling
app.get('/risky-operation', (req, res, next) => {
    try {
        // Simulated error
        throw new Error('Something went wrong');
    } catch (err) {
        next(err); // Pass error to error handling middleware
    }
});
            

Real-World Application Examples

Authentication Middleware


const authenticateUser = (req, res, next) => {
    const authToken = req.headers.authorization;
    
    if (!authToken) {
        return res.status(401).json({ error: 'No authentication token provided' });
    }

    try {
        // Verify token (simplified example)
        const user = verifyToken(authToken);
        req.user = user; // Attach user to request
        next();
    } catch (err) {
        res.status(401).json({ error: 'Invalid token' });
    }
};

// Protected route
app.get('/dashboard', authenticateUser, (req, res) => {
    res.json({ message: `Welcome ${req.user.name}` });
});
            

Request Rate Limiting


const rateLimit = () => {
    const requestCounts = {};
    
    return (req, res, next) => {
        const ip = req.ip;
        const now = Date.now();
        
        requestCounts[ip] = requestCounts[ip] || [];
        requestCounts[ip] = requestCounts[ip].filter(time => now - time < 60000);
        
        if (requestCounts[ip].length >= 100) {
            return res.status(429).json({ error: 'Too many requests' });
        }
        
        requestCounts[ip].push(now);
        next();
    };
};

app.use(rateLimit());
            

Best Practices and Common Patterns

When working with Express middleware and routing, follow these guidelines: