Understanding Express.js Middleware

Middleware functions are the backbone of Express.js applications. They have access to the request object (req), response object (res), and the next middleware function in the application's request-response cycle.

This comprehensive guide covers everything from basic middleware concepts to production-ready patterns used by companies at scale.

Middleware Fundamentals

Basic Middleware Structure

// Simple middleware function
function myMiddleware(req, res, next) {
    // Do something with req/res
    console.log('Request received:', req.method, req.url);
    
    // Pass control to next middleware
    next();
}

// Use middleware
app.use(myMiddleware);

Types of Middleware

  1. Application-level middleware: Bound to app instance
  2. Router-level middleware: Bound to router instance
  3. Error-handling middleware: Has 4 parameters (err, req, res, next)
  4. Built-in middleware: express.json(), express.static()
  5. Third-party middleware: helmet, cors, morgan

Essential Production Middleware

1. Security Middleware

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

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

// CORS configuration
const corsOptions = {
    origin: function (origin, callback) {
        const whitelist = process.env.ALLOWED_ORIGINS.split(',');
        if (whitelist.indexOf(origin) !== -1 || !origin) {
            callback(null, true);
        } else {
            callback(new Error('Not allowed by CORS'));
        }
    },
    credentials: true,
    optionsSuccessStatus: 200
};
app.use(cors(corsOptions));

// Rate limiting
const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // limit each IP to 100 requests per windowMs
    message: 'Too many requests from this IP',
    standardHeaders: true,
    legacyHeaders: false,
});
app.use('/api/', limiter);

// Stricter rate limit for auth endpoints
const authLimiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 5,
    skipSuccessfulRequests: true
});
app.use('/api/auth/', authLimiter);

2. Request Parsing Middleware

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

// Body parsers
app.use(express.json({ 
    limit: '10mb',
    verify: (req, res, buf) => {
        req.rawBody = buf;
    }
}));

app.use(express.urlencoded({ 
    extended: true,
    limit: '10mb'
}));

// Compression
app.use(compression({
    filter: (req, res) => {
        if (req.headers['x-no-compression']) {
            return false;
        }
        return compression.filter(req, res);
    },
    level: 6
}));

3. Logging Middleware

const morgan = require('morgan');
const winston = require('winston');
const fs = require('fs');
const path = require('path');

// Winston logger setup
const logger = winston.createLogger({
    level: 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    transports: [
        new winston.transports.File({ 
            filename: 'logs/error.log', 
            level: 'error' 
        }),
        new winston.transports.File({ 
            filename: 'logs/combined.log' 
        })
    ]
});

if (process.env.NODE_ENV !== 'production') {
    logger.add(new winston.transports.Console({
        format: winston.format.simple()
    }));
}

// Morgan HTTP request logging
const accessLogStream = fs.createWriteStream(
    path.join(__dirname, 'logs', 'access.log'),
    { flags: 'a' }
);

app.use(morgan('combined', { stream: accessLogStream }));

// Custom request logger
app.use((req, res, next) => {
    const start = Date.now();
    
    res.on('finish', () => {
        const duration = Date.now() - start;
        logger.info({
            method: req.method,
            url: req.url,
            status: res.statusCode,
            duration: `${duration}ms`,
            ip: req.ip,
            userAgent: req.get('user-agent')
        });
    });
    
    next();
});

4. Authentication Middleware

const jwt = require('jsonwebtoken');

// JWT authentication
function authenticateToken(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    
    if (!token) {
        return res.status(401).json({ error: 'No token provided' });
    }
    
    jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
        if (err) {
            return res.status(403).json({ error: 'Invalid token' });
        }
        
        req.user = user;
        next();
    });
}

// Optional authentication
function optionalAuth(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    
    if (token) {
        jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
            if (!err) {
                req.user = user;
            }
        });
    }
    
    next();
}

// Role-based authorization
function requireRole(...roles) {
    return (req, res, next) => {
        if (!req.user) {
            return res.status(401).json({ error: 'Not authenticated' });
        }
        
        if (!roles.includes(req.user.role)) {
            return res.status(403).json({ error: 'Insufficient permissions' });
        }
        
        next();
    };
}

// Usage
app.get('/api/protected', authenticateToken, (req, res) => {
    res.json({ data: 'Protected data', user: req.user });
});

app.get('/api/admin', authenticateToken, requireRole('admin'), (req, res) => {
    res.json({ data: 'Admin data' });
});

5. Validation Middleware

const { body, param, query, validationResult } = require('express-validator');

// Validation middleware factory
function validate(validations) {
    return async (req, res, next) => {
        await Promise.all(validations.map(validation => validation.run(req)));
        
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            return res.status(400).json({ 
                errors: errors.array().map(err => ({
                    field: err.param,
                    message: err.msg
                }))
            });
        }
        
        next();
    };
}

// Example validations
const userValidation = validate([
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 8 })
        .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
        .withMessage('Password must contain uppercase, lowercase, and number'),
    body('age').optional().isInt({ min: 18, max: 120 })
]);

const idValidation = validate([
    param('id').isMongoId()
]);

// Usage
app.post('/api/users', userValidation, createUser);
app.get('/api/users/:id', idValidation, getUser);

6. Error Handling Middleware

// Custom error class
class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = true;
        Error.captureStackTrace(this, this.constructor);
    }
}

// Async wrapper to catch errors
function asyncHandler(fn) {
    return (req, res, next) => {
        Promise.resolve(fn(req, res, next)).catch(next);
    };
}

// 404 handler
app.use((req, res, next) => {
    next(new AppError(`Cannot find ${req.originalUrl}`, 404));
});

// Global error handler
app.use((err, req, res, next) => {
    err.statusCode = err.statusCode || 500;
    err.status = err.status || 'error';
    
    // Log error
    logger.error({
        message: err.message,
        stack: err.stack,
        url: req.url,
        method: req.method
    });
    
    // Development error response
    if (process.env.NODE_ENV === 'development') {
        return res.status(err.statusCode).json({
            status: err.status,
            error: err,
            message: err.message,
            stack: err.stack
        });
    }
    
    // Production error response
    if (err.isOperational) {
        return res.status(err.statusCode).json({
            status: err.status,
            message: err.message
        });
    }
    
    // Programming or unknown errors
    return res.status(500).json({
        status: 'error',
        message: 'Something went wrong'
    });
});

// Usage
app.get('/api/users/:id', asyncHandler(async (req, res) => {
    const user = await User.findById(req.params.id);
    
    if (!user) {
        throw new AppError('User not found', 404);
    }
    
    res.json({ user });
}));

Advanced Middleware Patterns

1. Caching Middleware

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

function cache(duration) {
    return async (req, res, next) => {
        if (req.method !== 'GET') {
            return next();
        }
        
        const key = `cache:${req.originalUrl}`;
        
        try {
            const cachedResponse = await client.get(key);
            
            if (cachedResponse) {
                return res.json(JSON.parse(cachedResponse));
            }
            
            // Override res.json to cache response
            const originalJson = res.json.bind(res);
            res.json = (body) => {
                client.setex(key, duration, JSON.stringify(body));
                return originalJson(body);
            };
            
            next();
        } catch (error) {
            next(error);
        }
    };
}

// Usage
app.get('/api/products', cache(300), getProducts);

2. Request Context Middleware

const { v4: uuidv4 } = require('uuid');
const cls = require('cls-hooked');

const namespace = cls.createNamespace('request-context');

function requestContext(req, res, next) {
    namespace.run(() => {
        const requestId = uuidv4();
        namespace.set('requestId', requestId);
        namespace.set('userId', req.user?.id);
        
        res.setHeader('X-Request-ID', requestId);
        
        next();
    });
}

// Helper to get context anywhere
function getRequestContext() {
    return {
        requestId: namespace.get('requestId'),
        userId: namespace.get('userId')
    };
}

app.use(requestContext);

3. API Versioning Middleware

function apiVersion(version) {
    return (req, res, next) => {
        req.apiVersion = version;
        next();
    };
}

// V1 routes
const v1Router = express.Router();
v1Router.use(apiVersion('v1'));
v1Router.get('/users', getUsersV1);

// V2 routes
const v2Router = express.Router();
v2Router.use(apiVersion('v2'));
v2Router.get('/users', getUsersV2);

app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

4. Performance Monitoring

const prometheus = require('prom-client');

// Metrics
const httpRequestDuration = new prometheus.Histogram({
    name: 'http_request_duration_seconds',
    help: 'Duration of HTTP requests in seconds',
    labelNames: ['method', 'route', 'status_code']
});

const httpRequestTotal = new prometheus.Counter({
    name: 'http_requests_total',
    help: 'Total number of HTTP requests',
    labelNames: ['method', 'route', 'status_code']
});

function metricsMiddleware(req, res, next) {
    const start = process.hrtime();
    
    res.on('finish', () => {
        const duration = process.hrtime(start);
        const durationSeconds = duration[0] + duration[1] / 1e9;
        
        const route = req.route?.path || req.path;
        
        httpRequestDuration
            .labels(req.method, route, res.statusCode)
            .observe(durationSeconds);
        
        httpRequestTotal
            .labels(req.method, route, res.statusCode)
            .inc();
    });
    
    next();
}

app.use(metricsMiddleware);

// Metrics endpoint
app.get('/metrics', async (req, res) => {
    res.set('Content-Type', prometheus.register.contentType);
    res.end(await prometheus.register.metrics());
});

Middleware Execution Order

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

// 1. Security (first)
app.use(helmet());
app.use(cors(corsOptions));
app.use(limiter);

// 2. Request parsing
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(compression());

// 3. Logging and monitoring
app.use(morgan('combined'));
app.use(metricsMiddleware);
app.use(requestContext);

// 4. Static files
app.use(express.static('public'));

// 5. Authentication (if needed globally)
app.use(optionalAuth);

// 6. Routes
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

// 7. 404 handler
app.use(notFoundHandler);

// 8. Error handler (last)
app.use(errorHandler);

Testing Middleware

const request = require('supertest');
const express = require('express');

describe('Authentication Middleware', () => {
    let app;
    
    beforeEach(() => {
        app = express();
        app.use(express.json());
        app.get('/protected', authenticateToken, (req, res) => {
            res.json({ user: req.user });
        });
    });
    
    test('should reject request without token', async () => {
        const response = await request(app)
            .get('/protected');
        
        expect(response.status).toBe(401);
    });
    
    test('should accept valid token', async () => {
        const token = generateTestToken({ id: 1, email: '[email protected]' });
        
        const response = await request(app)
            .get('/protected')
            .set('Authorization', `Bearer ${token}`);
        
        expect(response.status).toBe(200);
        expect(response.body.user).toBeDefined();
    });
});

Performance Best Practices

  1. Order matters: Put frequently used middleware first
  2. Avoid heavy computation: Keep middleware fast
  3. Use async/await properly: Prevent blocking
  4. Cache when possible: Reduce redundant work
  5. Monitor performance: Track middleware execution time

Production Checklist

  • ✅ Security headers configured (helmet)
  • ✅ Rate limiting implemented
  • ✅ CORS properly configured
  • ✅ Request validation in place
  • ✅ Error handling comprehensive
  • ✅ Logging configured
  • ✅ Authentication/authorization working
  • ✅ Monitoring and metrics enabled
  • ✅ Compression enabled
  • ✅ Tests written for middleware

Conclusion

Mastering Express.js middleware is essential for building production-ready Node.js applications. From security to performance monitoring, middleware provides the foundation for scalable, maintainable applications.

Start with the basics, add security layers, implement proper error handling, and monitor everything. Your production application will thank you.