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. These patterns handle 80% of what you'll ever need.

Pattern 1: Request Logging (With Duration)

const morgan = require('morgan');

// Standard logging
app.use(morgan('combined'));

// Custom format with response time
app.use(morgan(':method :url :status :response-time ms - :res[content-length]', {
  skip: (req, res) => res.statusCode < 400, // Only log errors
}));

// DIY version (no dependencies):
app.use((req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    const status = res.statusCode;
    const color = status >= 500 ? 'red' : status >= 400 ? 'yellow' : 'green';
    console.log(`[${color}] ${req.method} ${req.path} ${status} ${duration}ms`);
  });

  next();
});
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Error Handling (Global Catch-All)

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

// Async wrapper — eliminates try/catch in every route
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Usage in routes:
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 middleware
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;

  // Don't leak stack traces in production
  const response = {
    error: {
      message: err.message,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
      ...(err.details && { details: err.details }),
    }
  };

  console.error(`[${statusCode}] ${err.message}`, err.stack);
  res.status(statusCode).json(response);
});
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Authentication Middleware

const jwt = require('jsonwebtoken');

// Flexible auth middleware factory
function requireAuth(options = {}) {
  const { required = true, role } = options;

  return (req, res, next) => {
    const header = req.headers.authorization;

    if (!header?.startsWith('Bearer ')) {
      if (required) return res.status(401).json({ error: 'Missing token' });
      return next(); // Optional auth — continue without user
    }

    const token = header.slice(7);

    try {
      const payload = jwt.verify(token, process.env.JWT_SECRET);
      req.user = payload; // Attach to request

      // Role check
      if (role && payload.role !== role) {
        return res.status(403).json({ error: 'Insufficient permissions' });
      }

      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('/profile', requireAuth(), (req, res) => {
  res.json(req.user);
});

app.get('/admin', requireAuth({ role: 'admin' }), (req, res) => {
  res.json({ secret: 'admin stuff' });
});

app.get('/feed', requireAuth({ required: false }), (req, res) => {
  // req.user might be undefined — show personalized or public feed
  res.json({ feed: req.user ? 'personalized' : 'public' });
});
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Request Validation

const { z } = require('zod');

function validate(schema) {
  return (req, res, next) => {
    const result = schema.safeParse({
      body: req.body,
      query: req.query,
      params: req.params,
    });

    if (!result.success) {
      const errors = result.error.flatten().fieldErrors;
      return res.status(400).json({ 
        error: 'Validation failed',
        details: errors 
      });
    }

    // Replace request data with validated/transformed data
    req.body = result.data.body;
    req.query = result.data.query;
    req.params = result.data.params;

    next();
  };
}

// Define schemas:
const createUserSchema = z.object({
  body: z.object({
    name: z.string().min(1).max(100),
    email: z.string().email(),
    password: z.string().min(8),
    role: z.enum(['user', 'admin']).default('user'),
  }),
});

// Usage:
app.post('/users', validate(createUserSchema), async (req, res) => {
  // req.body is already validated and typed
  const user = await User.create(req.body);
  res.status(201).json(user);
});
Enter fullscreen mode Exit fullscreen mode

Pattern 5: Request ID Tracing

const crypto = require('crypto');

app.use((req, res, next) => {
  // Accept existing request ID or generate new one
  const requestId = req.headers['x-request-id'] 
    || crypto.randomUUID();

  req.id = requestId;
  res.setHeader('X-Request-ID', requestId);

  // Include in all logs
  const originalLog = console.log;
  console.log = (...args) => {
    originalLog(`[${requestId}]`, ...args);
  };

  next();
});

// Now every log line includes the request ID:
// [a1b2c3d4] GET /api/users 200 45ms
// [a1b2c3d4] Database query: SELECT * FROM users 12ms
// [e5f6g7h8] POST /api/orders 201 120ms

// Use with a logging library (pino):
const pino = require('pino');
const logger = pino();

app.use((req, res, next) => {
  req.log = logger.child({ reqId: req.id });
  req.log.info({ req: { method: req.method, url: req.url } }, 'incoming');
  next();
});
Enter fullscreen mode Exit fullscreen mode

Putting It All Together

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

const app = express();

// Order matters!
app.use(helmet());                    // Security headers
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
app.use(express.json({ limit: '10kb' })); // Body parsing
app.use(requestId());                // Request tracing
app.use(morgan('combined'));         // HTTP logging

// Routes
app.use('/api/auth', authRoutes);     // No auth needed
app.use('/api/users', requireAuth(), userRoutes);
app.use('/api/admin', requireAuth({ role: 'admin' }), adminRoutes);

// Error handling (MUST be last)
app.use(notFoundHandler);
app.use(globalErrorHandler);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on :${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Pattern When to Use Complexity
Request logging Every app Low
Error handling Every app Medium
Auth middleware Protected routes Medium
Validation API endpoints Low
Request ID Debugging/production Low

Follow @armorbreak for more Node.js patterns and production-ready code.

Top comments (0)