Express
06 / 07

Middleware

Express Middleware

Middleware functions are the backbone of Express. They have access to the request and response objects and the next() function. Middleware can execute code, modify req/res, end the request-response cycle, or call the next middleware in the stack.

Writing Custom Middleware

// Application-level middleware (runs on every request)
app.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
  next(); // MUST call next() or the request hangs
});

// Middleware factory - returns middleware with custom options
function requestLogger(options = {}) {
  const { prefix = 'LOG' } = options;
  return (req, res, next) => {
    console.log(`[${prefix}] ${req.method} ${req.url}`);
    next();
  };
}
app.use(requestLogger({ prefix: 'API' }));

// Middleware that attaches data to req for downstream handlers
app.use(async (req, res, next) => {
  try {
    req.requestId = crypto.randomUUID();
    req.startTime = Date.now();
    next();
  } catch (err) {
    next(err); // Pass errors to error-handling middleware
  }
});

// Route-specific middleware
const requireAdmin = (req, res, next) => {
  if (req.user?.role !== 'admin') {
    return res.status(403).json({ error: 'Admin access required' });
  }
  next();
};

app.get('/admin/users', requireAdmin, (req, res) => {
  res.json(allUsers);
});

Error-Handling Middleware

Error-handling middleware has four parameters: (err, req, res, next). It must be defined AFTER all routes and regular middleware. Errors are passed to it by calling next(err) anywhere in the pipeline.

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

// 404 handler (place after all routes)
app.use((req, res, next) => {
  next(new AppError(`Route ${req.path} not found`, 404, 'NOT_FOUND'));
});

// Global error handler (4 params - Express identifies this as error handler)
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const message = err.isOperational ? err.message : 'Something went wrong';

  // Log unexpected errors
  if (!err.isOperational) {
    console.error('Unexpected error:', err);
  }

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

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

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

Third-Party Middleware

The Express ecosystem has well-maintained middleware for every common need. These are the packages you will reach for in almost every production application.

npm install cors helmet morgan cookie-parser compression express-rate-limit
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const compression = require('compression');
const rateLimit = require('express-rate-limit');

// Security headers
app.use(helmet());

// CORS configuration
app.use(cors({
  origin: ['https://myapp.com', 'https://www.myapp.com'],
  credentials: true,         // Allow cookies/auth headers
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
}));

// HTTP request logging
app.use(morgan('combined')); // 'dev' for development, 'combined' for production

// Cookie parsing
app.use(cookieParser(process.env.COOKIE_SECRET));
// Access cookies: req.cookies.name, req.signedCookies.name

// Gzip compression for responses
app.use(compression());

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                   // Max 100 requests per window per IP
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many requests, please try again later.' },
});
app.use('/api/', limiter);

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free