# Middleware Design

# Middleware Design Document

> **Project:** {{PROJECT_NAME}}
> **Version:** {{VERSION}}
> **Date:** {{DATE}}
> **Author:** {{AUTHOR}}
> **Status:** Draft | In Review | Approved
> **Reviewers:** {{REVIEWERS}}

## Document History
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 0.1     | {{DATE}} | {{AUTHOR}} | Initial draft |

---

## 1. Middleware Pipeline Overview

<!-- GUIDANCE: Show the complete ordered middleware stack that every request passes through. -->

```mermaid
sequenceDiagram
    participant Client
    participant CORS as 1. CORS
    participant Security as 2. Security Headers
    participant RequestID as 3. Request ID
    participant RateLimit as 4. Rate Limiter
    participant Logger as 5. Request Logger
    participant Auth as 6. Authentication
    participant Authz as 7. Authorization (RBAC)
    participant Validate as 8. Validation
    participant AuditLog as 9. Audit Logger
    participant Handler as Route Handler

    Client->>CORS: HTTP Request
    CORS->>Security: (CORS headers set)
    Security->>RequestID: (Security headers set)
    RequestID->>RateLimit: (X-Request-ID injected)
    RateLimit->>Logger: (Rate check passed)
    Logger->>Auth: (Request logged)
    Auth->>Authz: (JWT validated, user attached)
    Authz->>Validate: (Permissions verified)
    Validate->>AuditLog: (Input validated & sanitized)
    AuditLog->>Handler: (Audit record written)
    Handler-->>Client: Response
```

**Framework:** `{{NestJS / Express / Fastify / Hono}}`
**Execution order is strict** — changing order may break security guarantees.

---

## 2. Request Lifecycle

### 2.1 CORS Middleware

<!-- GUIDANCE: Define the CORS policy. Overly permissive CORS is a security vulnerability. -->

**Library:** `{{cors / @fastify/cors}}`

**Configuration:**

```ts
// config/cors.config.ts
export const corsConfig = {
  origin: (origin: string, callback: Function) => {
    const allowedOrigins = [
      'https://app.{{domain.com}}',
      'https://admin.{{domain.com}}',
      ...(process.env.NODE_ENV !== 'production'
        ? ['http://localhost:3000', 'http://localhost:3001']
        : []),
    ];

    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error(`Origin ${origin} not allowed by CORS`));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
  exposedHeaders: ['X-Request-ID', 'X-RateLimit-Limit', 'X-RateLimit-Remaining'],
  credentials: true,
  maxAge: 86400, // 24h preflight cache
};
```

**Performance impact:** < 0.1ms per request (header injection only)

---

### 2.2 Security Headers Middleware

**Library:** `helmet`

```ts
app.use(helmet({
  // Content Security Policy
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https://cdn.{{domain.com}}"],
      connectSrc: ["'self'", "https://api.{{domain.com}}"],
      frameSrc: ["'none'"],
      objectSrc: ["'none'"],
    },
  },
  // HTTP Strict Transport Security
  hsts: {
    maxAge: 31536000,       // 1 year
    includeSubDomains: true,
    preload: true,
  },
  // Other headers
  referrerPolicy: { policy: 'same-origin' },
  frameguard: { action: 'deny' },
  noSniff: true,            // X-Content-Type-Options: nosniff
  xssFilter: true,          // X-XSS-Protection (legacy browsers)
  hidePoweredBy: true,      // Remove X-Powered-By
}));
```

**Headers set:**

| Header | Value | Purpose |
|--------|-------|---------|
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains; preload` | Force HTTPS |
| `Content-Security-Policy` | (see above) | XSS prevention |
| `X-Frame-Options` | `DENY` | Clickjacking prevention |
| `X-Content-Type-Options` | `nosniff` | MIME sniffing prevention |
| `Referrer-Policy` | `same-origin` | Referrer privacy |

**Performance impact:** < 0.2ms per request

---

### 2.3 Request ID Middleware

**Purpose:** Correlate logs across services for distributed tracing.

```ts
// middleware/request-id.middleware.ts
export function requestIdMiddleware(req: Request, res: Response, next: NextFunction) {
  const requestId = req.headers['x-request-id'] as string
    || `req_${ulid()}`;

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

  // Bind to AsyncLocalStorage for log correlation
  requestContext.run({ requestId }, next);
}
```

**Format:** `req_{ulid}` — e.g., `req_01HX7M2K5N3P4Q5R6S7T8V9W0`

---

### 2.4 Authentication Middleware

<!-- GUIDANCE: Define JWT validation logic. This is a security-critical component — document carefully. -->

**Strategy:** JWT Bearer token validation

```ts
// guards/jwt.guard.ts
@Injectable()
export class JwtGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractToken(request);

    if (!token) throw new UnauthorizedException('No token provided');

    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: this.configService.get('JWT_SECRET'),
        algorithms: ['HS256'],
        clockTolerance: 10, // 10 second clock skew tolerance
      });

      // Attach to request for downstream use
      request.user = {
        id: payload.sub,
        email: payload.email,
        role: payload.role,
      };

      return true;
    } catch (error) {
      if (error instanceof TokenExpiredError) {
        throw new UnauthorizedException('TOKEN_EXPIRED');
      }
      throw new UnauthorizedException('INVALID_TOKEN');
    }
  }

  private extractToken(request: Request): string | null {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : null;
  }
}
```

**Performance impact:** ~2-5ms (crypto operation + optional DB lookup for token revocation)

**Token revocation check:** `{{Check Redis blocklist on each request | Check on logout only | Use short TTL — no revocation check}}`

---

### 2.5 Authorization Middleware (RBAC/ABAC)

<!-- GUIDANCE: Define role and permission enforcement. -->

**Model:** `{{RBAC (Role-Based) | ABAC (Attribute-Based) | Hybrid}}`

```ts
// decorators/roles.decorator.ts
export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);
export const RequirePermission = (permission: string) =>
  SetMetadata('permission', permission);

// guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<Role[]>('roles', context.getHandler());
    const requiredPermission = this.reflector.get<string>('permission', context.getHandler());

    if (!requiredRoles && !requiredPermission) return true; // Public route

    const { user } = context.switchToHttp().getRequest();

    if (requiredRoles && !requiredRoles.includes(user.role)) {
      throw new ForbiddenException(`Requires role: ${requiredRoles.join(' or ')}`);
    }

    if (requiredPermission && !this.hasPermission(user, requiredPermission)) {
      throw new ForbiddenException(`Requires permission: ${requiredPermission}`);
    }

    return true;
  }
}

// Usage on controller
@Get('users')
@Roles(Role.ADMIN)
@RequirePermission('users:read')
async listUsers() { ... }
```

**Role hierarchy:**

```
admin > manager > user > viewer > public
```

| Role | Capabilities |
|------|-------------|
| `admin` | Full access |
| `manager` | Read/write own org resources |
| `user` | Read/write own resources |
| `viewer` | Read-only |

---

### 2.6 Validation Middleware

<!-- GUIDANCE: Define input validation and sanitization approach. -->

**Library:** `class-validator + class-transformer` OR `zod`

```ts
// Global validation pipe (NestJS)
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,              // Strip unknown properties
  forbidNonWhitelisted: true,  // Throw if unknown properties present
  transform: true,              // Auto-transform to DTO types
  transformOptions: {
    enableImplicitConversion: true,
  },
}));
```

**Sanitization rules:**
- All string inputs: trim whitespace
- HTML content: sanitize with `DOMPurify` / `sanitize-html` (strip dangerous tags)
- SQL parameters: always use parameterized queries (ORM handles this)
- File uploads: validate MIME type by magic bytes (not just extension)

**Validation error format:**
```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "field": "email", "message": "must be an email" },
      { "field": "name", "message": "must be longer than 2 characters" }
    ]
  }
}
```

**Performance impact:** < 1ms for typical DTOs

---

### 2.7 Rate Limiting Middleware

<!-- GUIDANCE: Define the rate limiting algorithm, storage, and per-endpoint configuration. -->

**Library:** `{{@nestjs/throttler | express-rate-limit | rate-limiter-flexible}}`
**Storage:** `{{Redis}}` (shared across all replicas)

**Algorithms:**

| Algorithm | Library | Best For |
|-----------|---------|----------|
| Fixed window | `express-rate-limit` | Simple, low overhead |
| Sliding window | `rate-limiter-flexible` | Accurate, no burst at window edge |
| Token bucket | `rate-limiter-flexible` | Bursty traffic patterns |

**Selected algorithm:** `{{Sliding window}}`

**Configuration:**

```ts
const rateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'rl',
  points: 1000,         // Number of points
  duration: 60,         // Per 60 seconds
  blockDuration: 60,    // Block for 60s after exceeded
});

// Per-route overrides
const loginLimiter = new RateLimiterRedis({
  points: 5,
  duration: 900,        // 15 minutes
  blockDuration: 900,
});
```

**Key strategy:** `{{IP address | User ID (if authenticated) | IP + User ID}}`

**Performance impact:** ~1-2ms (Redis round-trip)

---

### 2.8 Audit Logging Middleware

<!-- GUIDANCE: Define what is audit logged and PII handling rules. -->

```ts
// interceptors/audit.interceptor.ts
@Injectable()
export class AuditInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const { method, path, user, requestId } = request;

    // Only audit mutating operations
    if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
      this.auditService.log({
        requestId,
        userId: user?.id,
        method,
        path,
        body: this.sanitizeBody(request.body), // Strip PII
        timestamp: new Date().toISOString(),
      });
    }

    return next.handle();
  }

  private sanitizeBody(body: Record<string, unknown>) {
    const REDACTED_FIELDS = ['password', 'token', 'creditCard', 'ssn'];
    return Object.fromEntries(
      Object.entries(body).map(([key, value]) =>
        REDACTED_FIELDS.includes(key) ? [key, '[REDACTED]'] : [key, value]
      )
    );
  }
}
```

**What IS logged:**
- User ID, request ID, timestamp, method, path
- Response status code, duration
- Mutation summaries (what changed, not full values)

**What is NEVER logged:**
- Passwords, tokens, API keys
- Payment card data
- Full PII fields (log field names but not values for sensitive fields)

**Audit log retention:** `{{1 year}}` (compliance requirement: `{{GDPR / SOC2 / internal}}`)

---

### 2.9 Error Handling Middleware

<!-- GUIDANCE: Define the global exception handler. All errors must be normalized before reaching the client. -->

```ts
// filters/global-exception.filter.ts
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    let status = 500;
    let code = 'INTERNAL_ERROR';
    let message = 'An unexpected error occurred';
    let details: unknown[] = [];

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const exceptionResponse = exception.getResponse() as any;
      code = exceptionResponse.code ?? 'HTTP_ERROR';
      message = exceptionResponse.message ?? exception.message;
      details = exceptionResponse.details ?? [];
    }

    // Log 5xx errors (not 4xx — those are client errors)
    if (status >= 500) {
      this.logger.error('Unhandled exception', { exception, requestId: request.requestId });
      this.sentryService.captureException(exception);
    }

    response.status(status).json({
      error: {
        code,
        message,
        details,
        requestId: request.requestId,
        timestamp: new Date().toISOString(),
      },
    });
  }
}
```

---

## 3. Custom Middleware Development Guide

<!-- GUIDANCE: Define the standard for writing new middleware. -->

**Template for new middleware:**

```ts
// middleware/{{name}}.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class {{Name}}Middleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction): void {
    // 1. Extract needed data from request
    // 2. Perform validation/enrichment/logging
    // 3. Set data on request object if needed
    // 4. Call next() or throw HttpException

    next();
  }
}
```

**Requirements for new middleware:**
- [ ] Handles errors without crashing the process
- [ ] Calls `next()` exactly once (or throws)
- [ ] Does not block async operations without async/await
- [ ] Performance impact documented
- [ ] Unit tests covering happy path + error path

---

## 4. Middleware Ordering & Dependencies

```
Request → [1] → [2] → [3] → [4] → [5] → [6] → [7] → [8] → [9] → Handler

[1] CORS          — No dependencies
[2] Security      — No dependencies
[3] Request ID    — Must be before Logger (Logger reads requestId)
[4] Rate Limiter  — Must be after Request ID (uses requestId for key)
[5] Logger        — Must be after Request ID
[6] Auth          — Must be after Logger (Logger should log auth failures)
[7] Authorization — MUST be after Auth (requires user on request)
[8] Validation    — MUST be after Auth (DTOs may reference user context)
[9] Audit Logger  — MUST be after Auth (logs user ID)
```

**NEVER reorder middleware without reviewing this dependency chain.**

---

## 5. Performance Impact Per Middleware

| Middleware | Avg Latency Added | P99 Latency Added | Notes |
|-----------|------------------|------------------|-------|
| CORS | 0.05ms | 0.1ms | Header injection only |
| Security Headers (Helmet) | 0.1ms | 0.2ms | Header injection only |
| Request ID | 0.1ms | 0.2ms | ID generation |
| Rate Limiter | 1.5ms | 5ms | Redis round-trip |
| Request Logger | 0.5ms | 1ms | Async log write |
| Authentication (JWT) | 3ms | 8ms | Crypto + optional Redis |
| Authorization | 0.5ms | 1ms | In-memory role check |
| Validation | 0.8ms | 2ms | Schema parsing |
| Audit Logger | 0.5ms | 1ms | Async DB write |
| **Total** | **~7ms** | **~18ms** | Middleware overhead |

**Target: middleware overhead < 10ms P50, < 25ms P99.**

---

## 6. Testing Strategy for Middleware

```ts
// Example unit test for Auth middleware
describe('JwtGuard', () => {
  it('should attach user to request on valid token', async () => {
    const token = generateTestToken({ sub: 'usr_123', role: 'user' });
    const mockRequest = { headers: { authorization: `Bearer ${token}` } };
    const result = await guard.canActivate(createMockContext(mockRequest));
    expect(result).toBe(true);
    expect(mockRequest.user).toMatchObject({ id: 'usr_123', role: 'user' });
  });

  it('should throw UnauthorizedException on expired token', async () => {
    const expiredToken = generateExpiredToken();
    const mockRequest = { headers: { authorization: `Bearer ${expiredToken}` } };
    await expect(guard.canActivate(createMockContext(mockRequest)))
      .rejects.toThrow('TOKEN_EXPIRED');
  });
});
```

**Test coverage requirements:**
- Each middleware: ≥ 90% line coverage
- Security middleware (Auth, AuthZ, Validation): 100% branch coverage

---

## 7. Configuration Options Per Middleware

| Middleware | Environment Variable | Default | Description |
|-----------|---------------------|---------|-------------|
| CORS | `CORS_ORIGINS` | `localhost:3000` | Comma-separated allowed origins |
| Rate Limit | `RATE_LIMIT_POINTS` | `1000` | Requests per window |
| Rate Limit | `RATE_LIMIT_DURATION` | `60` | Window size in seconds |
| Rate Limit (auth) | `AUTH_RATE_LIMIT_POINTS` | `5` | Login attempts per window |
| Audit Log | `AUDIT_LOG_RETENTION_DAYS` | `365` | How long to keep audit records |
| Request Body | `MAX_REQUEST_BODY_SIZE` | `1mb` | Max request body size |

---

## Approval
| Role | Name | Date | Signature |
|------|------|------|-----------|
| Author | | | |
| Backend Lead | | | |
| Security Lead | | | |
| Tech Lead | | | |