Files
flyer-crawler.projectium.com/docs/adr/0043-express-middleware-pipeline.md
Torben Sorensen e14c19c112
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m0s
linting docs + some fixes go claude and gemini
2026-01-09 22:38:57 -08:00

11 KiB

ADR-043: Express Middleware Pipeline Architecture

Date: 2026-01-09

Status: Accepted

Implemented: 2026-01-09

Context

The Express application uses a layered middleware pipeline to handle cross-cutting concerns:

  1. Security: Helmet headers, CORS, rate limiting.
  2. Parsing: JSON body, URL-encoded, cookies.
  3. Authentication: Session management, JWT verification.
  4. Validation: Request body/params validation.
  5. File Handling: Multipart form data, file uploads.
  6. Error Handling: Centralized error responses.

Middleware ordering is critical - incorrect ordering can cause security vulnerabilities or broken functionality. This ADR documents the canonical middleware order and patterns.

Decision

We will establish a strict middleware ordering convention:

  1. Security First: Security headers and protections apply to all requests.
  2. Parsing Before Logic: Body/cookie parsing before route handlers.
  3. Auth Before Routes: Authentication middleware before protected routes.
  4. Validation At Route Level: Per-route validation middleware.
  5. Error Handler Last: Centralized error handling catches all errors.

Design Principles

  • Defense in Depth: Multiple security layers.
  • Fail-Fast: Reject bad requests early in the pipeline.
  • Explicit Ordering: Document and enforce middleware order.
  • Route-Level Flexibility: Specific middleware per route as needed.

Implementation Details

Global Middleware Order

Located in src/server.ts:

import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import { requestTimeoutMiddleware } from './middleware/timeout.middleware';
import { rateLimiter } from './middleware/rateLimit.middleware';
import { errorHandler } from './middleware/errorHandler.middleware';

const app = express();

// ============================================
// LAYER 1: Security Headers & Protections
// ============================================
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "'unsafe-inline'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", 'data:', 'blob:'],
      },
    },
  }),
);
app.use(
  cors({
    origin: process.env.FRONTEND_URL,
    credentials: true,
  }),
);

// ============================================
// LAYER 2: Request Limits & Timeouts
// ============================================
app.use(requestTimeoutMiddleware(30000)); // 30s default
app.use(rateLimiter); // Rate limiting per IP

// ============================================
// LAYER 3: Body & Cookie Parsing
// ============================================
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(cookieParser());

// ============================================
// LAYER 4: Static Assets (before auth)
// ============================================
app.use('/flyer-images', express.static('flyer-images'));

// ============================================
// LAYER 5: Authentication Setup
// ============================================
app.use(passport.initialize());
app.use(passport.session());

// ============================================
// LAYER 6: Routes (with per-route middleware)
// ============================================
app.use('/api/auth', authRoutes);
app.use('/api/flyers', flyerRoutes);
app.use('/api/admin', adminRoutes);
// ... more routes

// ============================================
// LAYER 7: Error Handling (must be last)
// ============================================
app.use(errorHandler);

Validation Middleware

Located in src/middleware/validation.middleware.ts:

import { z } from 'zod';
import { Request, Response, NextFunction } from 'express';
import { ValidationError } from '../services/db/errors.db';

export const validate = <T extends z.ZodType>(schema: T) => {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse({
      body: req.body,
      query: req.query,
      params: req.params,
    });

    if (!result.success) {
      const errors = result.error.errors.map((err) => ({
        path: err.path.join('.'),
        message: err.message,
      }));
      return next(new ValidationError(errors));
    }

    // Attach validated data to request
    req.validated = result.data;
    next();
  };
};

// Usage in routes:
router.post('/flyers', authenticate, validate(CreateFlyerSchema), flyerController.create);

File Upload Middleware

Located in src/middleware/fileUpload.middleware.ts:

import multer from 'multer';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'flyer-images/');
  },
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname);
    cb(null, `${uuidv4()}${ext}`);
  },
});

const fileFilter = (req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true);
  } else {
    cb(new Error('Invalid file type'));
  }
};

export const uploadFlyer = multer({
  storage,
  fileFilter,
  limits: {
    fileSize: 10 * 1024 * 1024, // 10MB
    files: 10, // Max 10 files per request
  },
});

// Usage:
router.post('/flyers/upload', uploadFlyer.array('files', 10), flyerController.upload);

Authentication Middleware

Located in src/middleware/auth.middleware.ts:

import passport from 'passport';
import { Request, Response, NextFunction } from 'express';

// Require authenticated user
export const authenticate = (req: Request, res: Response, next: NextFunction) => {
  passport.authenticate('jwt', { session: false }, (err, user) => {
    if (err) return next(err);
    if (!user) {
      return res.status(401).json({ error: 'Unauthorized' });
    }
    req.user = user;
    next();
  })(req, res, next);
};

// Require admin role
export const requireAdmin = (req: Request, res: Response, next: NextFunction) => {
  if (!req.user?.role || req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Forbidden' });
  }
  next();
};

// Optional auth (attach user if present, continue if not)
export const optionalAuth = (req: Request, res: Response, next: NextFunction) => {
  passport.authenticate('jwt', { session: false }, (err, user) => {
    if (user) req.user = user;
    next();
  })(req, res, next);
};

Error Handler Middleware

Located in src/middleware/errorHandler.middleware.ts:

import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { logger } from '../services/logger.server';
import { ValidationError, NotFoundError, UniqueConstraintError } from '../services/db/errors.db';

export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
  const errorId = uuidv4();

  // Log error with context
  logger.error(
    {
      errorId,
      err,
      path: req.path,
      method: req.method,
      userId: req.user?.user_id,
    },
    'Request error',
  );

  // Map error types to HTTP responses
  if (err instanceof ValidationError) {
    return res.status(400).json({
      success: false,
      error: { code: 'VALIDATION_ERROR', message: err.message, details: err.errors },
      meta: { errorId },
    });
  }

  if (err instanceof NotFoundError) {
    return res.status(404).json({
      success: false,
      error: { code: 'NOT_FOUND', message: err.message },
      meta: { errorId },
    });
  }

  if (err instanceof UniqueConstraintError) {
    return res.status(409).json({
      success: false,
      error: { code: 'CONFLICT', message: err.message },
      meta: { errorId },
    });
  }

  // Default: Internal Server Error
  return res.status(500).json({
    success: false,
    error: {
      code: 'INTERNAL_ERROR',
      message: process.env.NODE_ENV === 'production' ? 'An unexpected error occurred' : err.message,
    },
    meta: { errorId },
  });
};

Request Timeout Middleware

export const requestTimeoutMiddleware = (timeout: number) => {
  return (req: Request, res: Response, next: NextFunction) => {
    res.setTimeout(timeout, () => {
      if (!res.headersSent) {
        res.status(503).json({
          success: false,
          error: { code: 'TIMEOUT', message: 'Request timed out' },
        });
      }
    });
    next();
  };
};

Route-Level Middleware Patterns

Protected Route with Validation

router.put(
  '/flyers/:flyerId',
  authenticate, // 1. Auth check
  validate(UpdateFlyerSchema), // 2. Input validation
  flyerController.update, // 3. Handler
);

Admin-Only Route

router.delete(
  '/admin/users/:userId',
  authenticate, // 1. Auth check
  requireAdmin, // 2. Role check
  validate(DeleteUserSchema), // 3. Input validation
  adminController.deleteUser, // 4. Handler
);

File Upload Route

router.post(
  '/flyers/upload',
  authenticate, // 1. Auth check
  uploadFlyer.array('files', 10), // 2. File handling
  validate(UploadFlyerSchema), // 3. Metadata validation
  flyerController.upload, // 4. Handler
);

Public Route with Optional Auth

router.get(
  '/flyers/:flyerId',
  optionalAuth, // 1. Attach user if present
  flyerController.getById, // 2. Handler (can check req.user)
);

Consequences

Positive

  • Security: Defense-in-depth with multiple security layers.
  • Consistency: Predictable request processing order.
  • Maintainability: Clear separation of concerns.
  • Debuggability: Errors caught and logged centrally.
  • Flexibility: Per-route middleware composition.

Negative

  • Order Sensitivity: Middleware order bugs can be subtle.
  • Performance: Many middleware layers add latency.
  • Complexity: New developers must understand the pipeline.

Mitigation

  • Document middleware order in comments (as shown above).
  • Use integration tests that verify middleware chain behavior.
  • Profile middleware performance in production.

Key Files

  • src/server.ts - Global middleware registration
  • src/middleware/validation.middleware.ts - Zod validation
  • src/middleware/fileUpload.middleware.ts - Multer configuration
  • src/middleware/multer.middleware.ts - File upload handling
  • src/middleware/errorHandler.middleware.ts - Error handling (implicit)