Express.js Middleware Patterns: Building Robust APIs

Express.js middleware is the backbone of robust Node.js applications. Understanding middleware patterns will help you build scalable, maintainable APIs that handle real-world requirements elegantly.

Understanding Middleware Flow

Middleware functions execute sequentially and have access to the request object (req), response object (res), and the next middleware function in the application’s request-response cycle.

const express = require('express');
const app = express();

// Basic middleware structure
app.use((req, res, next) => {
    console.log('Time:', Date.now());
    next(); // Pass control to next middleware
});

// Route-specific middleware
app.get('/protected', authenticate, authorize, (req, res) => {
    res.json({ message: 'Access granted' });
});

Authentication Middleware

JWT Authentication

const jwt = require('jsonwebtoken');

const authenticate = (req, res, next) => {
    const token = req.header('Authorization')?.replace('Bearer ', '');
    
    if (!token) {
        return res.status(401).json({ error: 'Access denied. No token provided.' });
    }
    
    try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.user = decoded;
        next();
    } catch (error) {
        res.status(400).json({ error: 'Invalid token.' });
    }
};

// Usage
app.get('/profile', authenticate, (req, res) => {
    res.json({ user: req.user });
});

Role-Based Authorization

const authorize = (roles = []) => {
    return (req, res, next) => {
        if (!req.user) {
            return res.status(401).json({ error: 'Unauthorized' });
        }
        
        if (roles.length && !roles.includes(req.user.role)) {
            return res.status(403).json({ error: 'Forbidden' });
        }
        
        next();
    };
};

// Usage
app.delete('/admin/users/:id', 
    authenticate, 
    authorize(['admin']), 
    deleteUser
);

Validation Middleware

Request Validation with Joi

const Joi = require('joi');

const validate = (schema) => {
    return (req, res, next) => {
        const { error } = schema.validate(req.body);
        if (error) {
            return res.status(400).json({
                error: 'Validation failed',
                details: error.details.map(detail => detail.message)
            });
        }
        next();
    };
};

// Define schemas
const userSchema = Joi.object({
    name: Joi.string().min(2).max(50).required(),
    email: Joi.string().email().required(),
    password: Joi.string().min(8).required()
});

// Usage
app.post('/users', validate(userSchema), createUser);

Custom Validation Middleware

const validateUser = (req, res, next) => {
    const { name, email, password } = req.body;
    const errors = [];
    
    if (!name || name.length < 2) {
        errors.push('Name must be at least 2 characters long');
    }
    
    if (!email || !/\S+@\S+\.\S+/.test(email)) {
        errors.push('Valid email is required');
    }
    
    if (!password || password.length < 8) {
        errors.push('Password must be at least 8 characters long');
    }
    
    if (errors.length > 0) {
        return res.status(400).json({ errors });
    }
    
    next();
};

Error Handling Middleware

Global Error Handler

const errorHandler = (err, req, res, next) => {
    console.error(err.stack);
    
    // Mongoose validation error
    if (err.name === 'ValidationError') {
        const errors = Object.values(err.errors).map(e => e.message);
        return res.status(400).json({
            error: 'Validation Error',
            details: errors
        });
    }
    
    // JWT errors
    if (err.name === 'JsonWebTokenError') {
        return res.status(401).json({ error: 'Invalid token' });
    }
    
    // MongoDB duplicate key error
    if (err.code === 11000) {
        return res.status(400).json({
            error: 'Duplicate field value',
            field: Object.keys(err.keyValue)[0]
        });
    }
    
    // Default error
    res.status(err.status || 500).json({
        error: err.message || 'Internal Server Error'
    });
};

// Use at the end of middleware stack
app.use(errorHandler);

Async Error Wrapper

const asyncHandler = (fn) => {
    return (req, res, next) => {
        Promise.resolve(fn(req, res, next)).catch(next);
    };
};

// Usage with async route handlers
app.get('/users', asyncHandler(async (req, res) => {
    const users = await User.find();
    res.json(users);
}));

Rate Limiting Middleware

Basic Rate Limiting

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // limit each IP to 100 requests per windowMs
    message: {
        error: 'Too many requests from this IP, please try again later.'
    },
    standardHeaders: true,
    legacyHeaders: false,
});

app.use('/api/', limiter);

Custom Rate Limiting with Redis

const redis = require('redis');
const client = redis.createClient();

const customRateLimit = (maxRequests = 100, windowMs = 15 * 60 * 1000) => {
    return async (req, res, next) => {
        const key = `rate_limit:${req.ip}`;
        
        try {
            const current = await client.incr(key);
            
            if (current === 1) {
                await client.expire(key, Math.ceil(windowMs / 1000));
            }
            
            if (current > maxRequests) {
                return res.status(429).json({
                    error: 'Rate limit exceeded',
                    retryAfter: await client.ttl(key)
                });
            }
            
            res.set({
                'X-RateLimit-Limit': maxRequests,
                'X-RateLimit-Remaining': Math.max(0, maxRequests - current),
                'X-RateLimit-Reset': new Date(Date.now() + windowMs)
            });
            
            next();
        } catch (error) {
            console.error('Rate limiting error:', error);
            next(); // Fail open
        }
    };
};

Logging and Monitoring Middleware

Request Logging

const morgan = require('morgan');

// Custom log format
morgan.token('user', (req) => {
    return req.user ? req.user.id : 'anonymous';
});

const logFormat = ':method :url :status :res[content-length] - :response-time ms - User: :user';

app.use(morgan(logFormat));

Performance Monitoring

const performanceMiddleware = (req, res, next) => {
    const start = process.hrtime.bigint();
    
    res.on('finish', () => {
        const end = process.hrtime.bigint();
        const duration = Number(end - start) / 1000000; // Convert to milliseconds
        
        console.log(`${req.method} ${req.url} - ${duration.toFixed(2)}ms`);
        
        // Log slow requests
        if (duration > 1000) {
            console.warn(`Slow request detected: ${req.method} ${req.url} - ${duration.toFixed(2)}ms`);
        }
    });
    
    next();
};

app.use(performanceMiddleware);

Security Middleware

CORS Configuration

const cors = require('cors');

const corsOptions = {
    origin: (origin, callback) => {
        const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
        
        if (!origin || allowedOrigins.includes(origin)) {
            callback(null, true);
        } else {
            callback(new Error('Not allowed by CORS'));
        }
    },
    credentials: true,
    optionsSuccessStatus: 200
};

app.use(cors(corsOptions));

Security Headers

const helmet = require('helmet');

app.use(helmet({
    contentSecurityPolicy: {
        directives: {
            defaultSrc: ["'self'"],
            styleSrc: ["'self'", "'unsafe-inline'"],
            scriptSrc: ["'self'"],
            imgSrc: ["'self'", "data:", "https:"],
        },
    },
    hsts: {
        maxAge: 31536000,
        includeSubDomains: true,
        preload: true
    }
}));

Middleware Composition

Combining Middleware

const apiMiddleware = [
    express.json({ limit: '10mb' }),
    express.urlencoded({ extended: true }),
    cors(corsOptions),
    helmet(),
    morgan('combined'),
    performanceMiddleware
];

app.use('/api', ...apiMiddleware);

// Route-specific middleware composition
const protectedRoute = [authenticate, authorize(['user', 'admin'])];
app.get('/api/protected', ...protectedRoute, handler);

Middleware patterns are the foundation of maintainable Express applications. By composing small, focused middleware functions, you create flexible and testable code that can evolve with your application’s needs.