Express.js Middleware Guide

Comprehensive reference for Express.js middleware patterns including built-in, third-party, and custom middleware with practical code examples.

1. Middleware Basics โ€” app.use()

Middleware functions have access to req, res, and next. Call next() to pass control to the next middleware.

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

// Application-level middleware (runs for every request)
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path} โ€” ${Date.now()}`);
  next(); // must call next() or send a response
});

// Path-scoped middleware (only runs for /api/*)
app.use('/api', (req, res, next) => {
  req.apiVersion = 'v1';
  next();
});

// Multiple middleware functions in one call
app.use(express.json(), express.urlencoded({ extended: true }));

app.listen(3000);

2. Custom Middleware Patterns

Request Logger

// middleware/logger.js
function requestLogger(options = {}) {
  const { format = 'combined' } = options;
  return (req, res, next) => {
    const start = Date.now();
    res.on('finish', () => {
      const duration = Date.now() - start;
      console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`);
    });
    next();
  };
}
module.exports = requestLogger;

// Usage
app.use(requestLogger({ format: 'short' }));

Authentication Middleware

// middleware/auth.js
const jwt = require('jsonwebtoken');

function requireAuth(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }
  try {
    const token = authHeader.slice(7);
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
}

// Role-based guard factory
function requireRole(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user?.role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

module.exports = { requireAuth, requireRole };

// Usage
app.get('/admin', requireAuth, requireRole('admin'), (req, res) => {
  res.json({ message: 'Welcome admin' });
});

3. Error-Handling Middleware

Error middleware takes 4 parameters: (err, req, res, next). It must be registered last.

// Async error wrapper โ€” avoids try/catch in every route
const asyncHandler = fn => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

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

// Route using asyncHandler
app.get('/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);
}));

// Global error handler (must be last app.use)
app.use((err, req, res, next) => {
  const status = err.statusCode || 500;
  const message = err.isOperational ? err.message : 'Internal server error';
  if (process.env.NODE_ENV === 'development') console.error(err.stack);
  res.status(status).json({ error: message });
});

4. CORS Middleware

const cors = require('cors');

// Simple โ€” allow all origins
app.use(cors());

// Production config
const corsOptions = {
  origin: (origin, callback) => {
    const allowed = ['https://app.example.com', 'https://admin.example.com'];
    if (!origin || allowed.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400, // preflight cache 24h
};
app.use(cors(corsOptions));

// Per-route CORS override
app.get('/public', cors({ origin: '*' }), (req, res) => {
  res.json({ data: 'public endpoint' });
});

5. Body-Parser & Helmet

const helmet = require('helmet');

// Helmet โ€” sets secure HTTP headers
app.use(helmet());

// Fine-grained helmet config
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'nonce-GENERATED_NONCE'"],
      styleSrc: ["'self'", 'https://fonts.googleapis.com'],
      imgSrc: ["'self'", 'data:', 'https:'],
    },
  },
  hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
  referrerPolicy: { policy: 'same-origin' },
}));

// Body parsing (built-in since Express 4.16)
app.use(express.json({ limit: '10kb' }));          // JSON bodies
app.use(express.urlencoded({ extended: true }));    // form bodies
app.use(express.raw({ type: 'application/octet-stream', limit: '5mb' }));

6. Middleware Execution Order

OrderMiddlewarePurpose
1helmet()Security headers โ€” run first
2cors()CORS headers before any logic
3express.json()Parse request body
4rateLimit()Throttle before heavy work
5requireAuth()Authenticate user
6routesBusiness logic
7errorHandlerMust be last