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 Express's superpower. Here are the patterns I use in every project.

What is Middleware?

Request → Middleware 1 → Middleware 2 → ... → Route Handler → Response
              ↓              ↓
         (auth check)   (logging)
Enter fullscreen mode Exit fullscreen mode
// Basic structure
function middleware(req, res, next) {
  // Do something with request/response
  if (/* condition */) {
    return next();        // Pass to next middleware/handler
  }
  res.status(403).json({ error: 'Forbidden' }); // Or end here
}
Enter fullscreen mode Exit fullscreen mode

Pattern 1: Authentication Middleware

// Simple API key auth
function apiKeyAuth(req, res, next) {
  const key = req.headers['x-api-key'];

  if (!key) {
    return res.status(401).json({ error: 'API key required' });
  }

  const user = validateApiKey(key); // Your validation logic
  if (!user) {
    return res.status(401).json({ error: 'Invalid API key' });
  }

  req.user = user; // Attach to request for downstream use
  next();
}

// JWT auth (more common for web apps)
function jwtAuth(req, res, next) {
  const header = req.headers.authorization;

  if (!header?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }

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

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.user = payload;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// Usage
app.get('/api/profile', jwtAuth, (req, res) => {
  res.json({ user: req.user }); // req.user is available!
});
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Error Handling Middleware

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

// Async handler wrapper (catches errors automatically)
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

// Routes using asyncHandler
app.get('/users', asyncHandler(async (req, res) => {
  const users = await User.findAll(); // If this throws → caught by error middleware
  res.json(users);
}));

// Global error handler (MUST be last middleware)
app.use((err, req, res, _next) => {
  console.error('Error:', err);

  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: err.message,
      code: err.code,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
    });
  }

  // Handle known errors
  if (err.name === 'ValidationError') {
    return res.status(422).json({ error: err.message, details: err.details });
  }

  if (err.code === 'SQLITE_CONSTRAINT') {
    return res.status(409).json({ error: 'Resource already exists' });
  }

  // Unknown errors — don't leak details in production
  const statusCode = err.statusCode || 500;
  const message = process.env.NODE_ENV === 'production'
    ? 'Internal server error'
    : err.message;

  res.status(statusCode).json({ error: message });
});
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Request Validation

// Using express-validator (or zod, joi)
const { body, param, query, validationResult } = require('express-validator');

// Validation rules
const createUserRules = [
  body('name')
    .trim()
    .isLength({ min: 2, max: 50 })
    .withMessage('Name must be 2-50 characters'),
  body('email')
    .isEmail()
    .normalizeEmail()
    .withMessage('Must be a valid email'),
  body('password')
    .isLength({ min: 8 })
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
    .withMessage('Password must have uppercase, lowercase, and number'),
];

// Validation middleware
const validate = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({
      error: 'Validation failed',
      details: errors.array().map(e => ({ field: e.path, message: e.msg })),
    });
  }
  next();
};

// Usage
app.post(
  '/api/users',
  createUserRules,     // Define rules
  validate,            // Check results
  asyncHandler(async (req, res) => {
    // Data is validated and clean!
    const user = await User.create(req.body);
    res.status(201).json(user);
  }),
);
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Request Logging & Timing

// Development logger (verbose)
if (process.env.NODE_ENV === 'development') {
  app.use((req, res, next) => {
    const start = Date.now();

    // Log request details
    console.log(`📥 ${req.method} ${req.originalUrl}`);

    // Log when response finishes
    res.on('finish', () => {
      const duration = Date.now() - start;
      const statusColor = res.statusCode < 400 ? '\x1b[32m' : '\x1b[31m';
      console.log(
        `${statusColor}${res.statusCode}\x1b[0m ` +
        `${req.method} ${req.originalUrl} - ${duration}ms`
      );
    });

    next();
  });
}

// Production logger (structured JSON)
const morgan = require('morgan');
app.use(morgan('combined', {
  stream: {
    write: (message) => {
      // Send to your logging service (Datadog, CloudWatch, etc.)
      logToService(message.trim());
    },
  },
}));
Enter fullscreen mode Exit fullscreen mode

Pattern 5: Rate Limiting + Security Headers

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

// Security headers (always include!)
app.use(helmet());

// Rate limiting per endpoint
const strictLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  standardHeaders: true,
});

const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 10,
  skipSuccessfulRequests: true,
});

// Apply to routes
app.use('/api/', strictLimiter);
app.post('/api/login', authLimiter, loginHandler);
app.post('/api/register', authLimiter, registerHandler);

// CORS configuration
const cors = require('cors');
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
}));
Enter fullscreen mode Exit fullscreen mode

Bonus: Compose Middleware

// Combine multiple middleware into one
function compose(...middlewares) {
  return (req, res, next) => {
    let index = 0;

    function run(i) {
      if (i >= middlewares.length) return next();

      middlewares[i](req, res, (err) => {
        if (err) return next(err);
        run(i + 1);
      });
    }

    run(0);
  };
}

// Usage
const authenticatedRoute = compose(
  cors(),
  rateLimit({ windowMs: 60000, max: 30 }),
  jwtAuth,
  attachPermissions,
);

app.get('/api/dashboard', authenticatedRoute, dashboardHandler);
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Middleware Purpose Package
helmet() Security headers helmet
cors() Cross-origin requests cors
rateLimit() Throttle requests express-rate-limit
morgan() HTTP logging morgan
express.json() Parse JSON bodies built-in
cookieParser() Parse cookies cookie-parser
Custom asyncHandler Catch async errors custom
Custom validate Input validation express-validator/zod

What's your go-to Express middleware pattern?

Follow @armorbreak for more Node.js content.

Top comments (0)