DEV Community

Alex Chen
Alex Chen

Posted on

5 Express.js Middleware Patterns You'll Use in Every App

5 Express.js Middleware Patterns You'll Use in Every App

Middleware is what makes Express powerful. These patterns show up in every production app.

What Is Middleware?

// A function that has access to req, res, and next
function myMiddleware(req, res, next) {
  // Do something with the request
  console.log(`${req.method} ${req.path}`);

  // Either respond (end the chain)
  if (req.path === '/health') {
    return res.json({ status: 'ok' });
  }

  // Or pass control to the next middleware
  next();
}

app.use(myMiddleware);
Enter fullscreen mode Exit fullscreen mode

Pattern 1: Request Logging & Correlation IDs

import { randomUUID } from 'crypto';

// Add a unique ID to every request for tracing
function requestId(req, _res, next) {
  req.id = req.headers['x-request-id'] || randomUUID();
  next();
}

// Structured logging with context
function logger(req, _res, next) {
  const start = Date.now();

  _res.on('finish', () => {
    const duration = Date.now() - start;
    const logData = {
      id: req.id,
      method: req.method,
      url: req.originalUrl,
      status: _res.statusCode,
      duration: `${duration}ms`,
      ip: req.ip,
      userAgent: req.headers['user-agent'],
    };

    console.log(JSON.stringify(logData));
  });

  next();
}

app.use(requestId);
app.use(logger);

// Output: {"id":"abc123","method":"GET","url":"/api/users","status":200,"duration":"12ms","ip":"1.2.3.4"}
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Rate Limiting (Custom Implementation)

// Simple in-memory rate limiter
const rateLimits = new Map();

function rateLimit({ windowMs = 60000, max = 100 } = {}) {
  return function rateLimitMiddleware(req, res, next) {
    const key = req.ip;
    const now = Date.now();

    const record = rateLimits.get(key) || { count: 0, resetAt: now + windowMs };

    if (now > record.resetAt) {
      record.count = 0;
      record.resetAt = now + windowMs;
    }

    record.count++;
    rateLimits.set(key, record);

    // Add headers so clients know their limit
    res.setHeader('X-RateLimit-Limit', max);
    res.setHeader('X-RateLimit-Remaining', Math.max(0, max - record.count));
    res.setHeader('X-RateLimit-Reset', new Date(record.resetAt).toISOString());

    if (record.count > max) {
      return res.status(429).json({
        error: { code: 'RATE_LIMITED', message: 'Too many requests' }
      });
    }

    next();
  };
}

// Different limits for different routes
app.use('/api/auth/', rateLimit({ max: 5, windowMs: 60 * 1000 }));   // 5/min for auth
app.use('/api/',       rateLimit({ max: 100, windowMs: 60 * 1000 }));  // 100/min general

// Clean up old entries periodically
setInterval(() => {
  const now = Date.now();
  for (const [key, record] of rateLimits.entries()) {
    if (now > record.resetAt) rateLimits.delete(key);
  }
}, 5 * 60 * 1000);
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Authentication & Authorization

// JWT authentication middleware
function auth(req, res, next) {
  const header = req.headers.authorization;

  if (!header || !header.startsWith('Bearer ')) {
    return res.status(401).json({
      error: { code: 'AUTH_MISSING', message: 'Authorization required' }
    });
  }

  const token = header.split(' ')[1];

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.user = payload; // Attach user to request
    next();
  } catch (err) {
    return res.status(401).json({
      error: { code: 'TOKEN_INVALID', message: 'Invalid or expired token' }
    });
  }
}

// Role-based authorization (requires auth to run first)
function requireRole(...roles) {
  return function roleCheck(req, res, next) {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        error: { code: 'FORBIDDEN', message: `Requires role: ${roles.join(' or ')}` }
      });
    }
    next();
  };
}

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

app.delete('/api/users/:id', auth, requireRole('admin'), (req, res) => {
  // Only admins can delete users
});
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Error Handling Wrapper

// Wrap async route handlers to catch errors automatically
function asyncHandler(fn) {
  return function asyncHandlerWrapper(req, res, next) {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// Without wrapper — error handling is verbose
app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    res.json({ data: user });
  } catch (err) {
    next(err); // Must remember to call next(err)!
  }
});

// With wrapper — clean and safe
app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  res.json({ data: user }));
}));

// Works with all HTTP methods
app.post('/users', asyncHandler(async (req, res) => {
  const user = await User.create(req.body);
  res.status(201).json({ data: user });
}));
Enter fullscreen mode Exit fullscreen mode

Pattern 5: Validation Middleware

// Reusable validation using Zod schemas
const z = require('zod');

function validate(schema) {
  return function validateMiddleware(req, _res, next) {
    try {
      // Validate body, query, or params based on schema shape
      const result = schema.safeParse({
        body: req.body,
        query: req.query,
        params: req.params,
      });

      if (!result.success) {
        const errors = result.error.errors.map(e => ({
          field: e.path.join('.'),
          issue: e.message,
        }));

        return _res.status(422).json({
          error: { code: 'VALIDATION_ERROR', message: 'Invalid input', details: errors }
        });
      }

      // Replace request data with validated/transformed data
      Object.assign(req, result.data);
      next();
    } catch (err) {
      next(err);
    }
  };
}

// Define validation schemas
const createUserSchema = z.object({
  body: z.object({
    name: z.string().min(1).max(100),
    email: z.string().email(),
    password: z.string().min(8).regex(/^(?=.*[A-Z])(?=.*[0-9])/),
    role: z.enum(['user', 'admin']).optional().default('user'),
  }),
  query: z.object({}).optional(),
  params: z.object({}).optional(),
});

const updateUserSchema = z.object({
  params: z.object({ id: z.string().uuid() }),
  body: z.object({
    name: z.string().min(1).max(100).optional(),
    email: z.string().email().optional(),
  }).partial(), // All fields optional for updates
});

// Apply to routes
app.post('/users', validate(createUserSchema), asyncHandler(async (req, res) => {
  // req.body is already validated and typed!
  const user = await User.create(req.body.body);
  res.status(201).json({ data: user });
}));

app.put('/users/:id', validate(updateUserSchema), asyncHandler(async (req, res) => {
  const user = await User.update(req.body.params.id, req.body.body);
  res.json({ data: user });
}));
Enter fullscreen mode Exit fullscreen mode

The Complete Middleware Stack

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

// Order matters!

// 1. Security headers (always first)
app.use(helmet());

// 2. CORS configuration
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));

// 3. Request parsing (with limits)
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));

// 4. App-level middleware
app.use(requestId);
app.use(logger);

// 5. Health check (no auth needed)
app.get('/health', (_req, res) => {
  res.json({ status: 'ok', time: new Date().toISOString() });
});

// 6. Public routes
app.use('/api/public', publicRoutes);

// 7. Authenticated routes
app.use('/api', auth, authenticatedRoutes);

// 8. Admin routes
app.use('/admin', auth, requireRole('admin'), adminRoutes);

// 9. Error handler (MUST be last)
app.use((err, req, res, _next) => {
  console.error(`[ERR] ${req.id}:`, err);
  res.status(err.statusCode || 500).json({
    error: {
      code: err.code || 'INTERNAL_ERROR',
      message: process.env.NODE_ENV === 'production' ? 'Internal error' : err.message,
    },
  });
});

// 404 handler (no route matched)
app.use((_req, res) => {
  res.status(404).json({ error: { code: 'NOT_FOUND', message: 'Route not found' } });
});
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Pattern Package Needed When to Use
Request logging None (custom) Every app
Rate limiting express-rate-limit or custom Public APIs
Auth jsonwebtoken, passport Protected routes
Async handling None (one-liner) Any async route
Validation zod, joi, or yup Input from users
Error handling None (built-in) Always at the end
Security helmet Every public-facing app
CORS cors APIs used by browsers

What's your favorite Express middleware pattern? Share it!

Follow @armorbreak for more Node.js guides.

Top comments (0)