# 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`: ```typescript 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`: ```typescript import { z } from 'zod'; import { Request, Response, NextFunction } from 'express'; import { ValidationError } from '../services/db/errors.db'; export const validate = (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`: ```typescript 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`: ```typescript 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`: ```typescript 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 ```typescript 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 ```typescript router.put( '/flyers/:flyerId', authenticate, // 1. Auth check validate(UpdateFlyerSchema), // 2. Input validation flyerController.update, // 3. Handler ); ``` ### Admin-Only Route ```typescript 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 ```typescript 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 ```typescript 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) ## Related ADRs - [ADR-001](./0001-standardized-error-handling.md) - Error Handling - [ADR-003](./0003-standardized-input-validation-using-middleware.md) - Input Validation - [ADR-016](./0016-api-security-hardening.md) - API Security - [ADR-032](./0032-rate-limiting-strategy.md) - Rate Limiting - [ADR-033](./0033-file-upload-and-storage-strategy.md) - File Uploads