Skip to content

Latest commit

 

History

History
1250 lines (1001 loc) · 37 KB

File metadata and controls

1250 lines (1001 loc) · 37 KB

Middleware Documentation

0http-bun provides a comprehensive middleware system with built-in middlewares for common use cases. All middleware functions are TypeScript-ready and follow the standard middleware pattern.

Dependency Installation

⚠️ Important: Starting with v1.2.2, middleware dependencies are now optional and must be installed separately when needed. This reduces the framework's footprint and improves startup performance through lazy loading.

Install only the dependencies you need:

# For JWT Authentication middleware
bun install jose

# For Logger middleware
bun install pino

# For Prometheus Metrics middleware
bun install prom-client

Benefits of Lazy Loading:

  • 📦 Smaller Bundle: Only install what you use
  • Faster Startup: Dependencies loaded only when middleware is used
  • 💾 Lower Memory: Reduced initial memory footprint
  • 🔧 Better Control: Explicit dependency management

Table of Contents

Middleware Pattern

All middlewares in 0http-bun follow the standard pattern:

import {ZeroRequest, StepFunction} from '0http-bun'

type Middleware = (
  req: ZeroRequest,
  next: StepFunction,
) => Promise<Response> | Response

TypeScript Support

TypeScript type definitions are available for both the core framework and middleware modules:

// Core framework types (from root module)
import {ZeroRequest, StepFunction, RequestHandler} from '0http-bun'

// Middleware-specific types (from middleware module)
import type {
  LoggerOptions,
  JWTAuthOptions,
  RateLimitOptions,
  RateLimitStore,
  MemoryStore,
  CORSOptions,
  BodyParserOptions,
} from '0http-bun/lib/middleware'

// Import middleware functions
import {
  createLogger,
  createJWTAuth,
  createRateLimit,
  createPrometheusIntegration,
} from '0http-bun/lib/middleware'

Built-in Middlewares

All middleware can be imported from the main middleware module:

// Import all middleware from the middleware index
const {
  createBodyParser,
  createCORS,
  createJWTAuth,
  createLogger,
  createRateLimit,
  createPrometheusMiddleware,
  createMetricsHandler,
  createPrometheusIntegration,
} = require('0http-bun/lib/middleware')

For TypeScript:

// Import middleware functions
import {
  createBodyParser,
  createCORS,
  createJWTAuth,
  createLogger,
  createRateLimit,
  createPrometheusMiddleware,
  createMetricsHandler,
  createPrometheusIntegration,
} from '0http-bun/lib/middleware'

// Import types
import type {
  BodyParserOptions,
  CORSOptions,
  JWTAuthOptions,
  LoggerOptions,
  RateLimitOptions,
} from '0http-bun/lib/middleware'

Body Parser

Automatically parses request bodies based on Content-Type header.

No additional dependencies required - Uses Bun's built-in parsing capabilities.

const {createBodyParser} = require('0http-bun/lib/middleware')

const router = http()

// Basic usage
router.use(createBodyParser())

// Access parsed body
router.post('/api/data', (req) => {
  console.log(req.body) // Parsed body content
  return Response.json({received: req.body})
})

TypeScript Usage:

import {createBodyParser} from '0http-bun/lib/middleware'
import type {BodyParserOptions} from '0http-bun/lib/middleware'

// With custom configuration
const bodyParserOptions: BodyParserOptions = {
  json: {
    limit: 10 * 1024 * 1024, // 10MB
    strict: true,
  },
  urlencoded: {
    extended: true,
    limit: 1024 * 1024, // 1MB
  },
}

router.use(createBodyParser(bodyParserOptions))

Supported Content Types:

  • application/json - Parsed as JSON (strict matching; application/xml and other application/* types are no longer routed to the JSON parser)
  • application/x-www-form-urlencoded - Parsed as form data
  • multipart/form-data - Parsed as FormData
  • text/* - Parsed as plain text
  • application/octet-stream - Parsed as ArrayBuffer

Security Features:

  • JSON nesting depth limit (max 100) with string-aware scanning — brace characters inside JSON strings do not count toward the depth limit
  • Prototype pollution protection — multipart body and files objects use Object.create(null), and dangerous property names (__proto__, constructor, prototype, etc.) are blocked
  • Multipart filename sanitization — strips .., path separators (/, \), null bytes, and leading dots. The original filename is preserved in file.originalName
  • Size limit validationparseLimit() throws TypeError for unexpected types (e.g., null, false, objects) instead of silently defaulting to 1MB
  • Custom jsonParser safety — when a custom jsonParser function is provided, size limits are enforced before the parser is called
  • Empty body handling — empty or whitespace-only JSON bodies set req.body to undefined instead of silently returning {}
  • Raw body via Symbol — raw body text is stored via a Symbol (RAW_BODY_SYMBOL) instead of a public string property, preventing accidental serialization/logging

Accessing Raw Body:

import {RAW_BODY_SYMBOL} from '0http-bun/lib/middleware/body-parser'

router.post('/webhook', (req) => {
  const rawBody = req[RAW_BODY_SYMBOL] // Symbol.for('0http.rawBody')
  // Use rawBody for signature verification, etc.
})

Verify Function:

When using the verify option with createBodyParser, the raw body is available via the same symbol:

const bodyParser = createBodyParser({
  verify: (req, rawBody) => {
    // rawBody is the raw string before parsing
    const signature = req.headers.get('x-signature')
    if (!verifySignature(rawBody, signature)) {
      throw new Error('Invalid signature')
    }
  },
  // Note: any JSON parsing behavior (such as deferNext) is handled internally when verify is set.
})

CORS

Cross-Origin Resource Sharing middleware with flexible configuration.

No additional dependencies required - Built-in CORS implementation.

const {createCORS} = require('0http-bun/lib/middleware')

// Basic usage (allows all origins)
router.use(createCORS())

// Custom configuration
router.use(
  createCORS({
    origin: ['https://example.com', 'https://app.example.com'],
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    exposedHeaders: ['X-Total-Count'],
    credentials: true,
    maxAge: 86400, // Preflight cache duration (seconds)
    preflightContinue: false,
    optionsSuccessStatus: 204,
  }),
)

// Dynamic origin validation
// NOTE: null and missing origins are automatically rejected before calling the
// validator function, preventing bypass via sandboxed iframes.
router.use(
  createCORS({
    origin: (origin, req) => {
      // Custom logic to validate origin
      // `origin` is guaranteed to be a non-null, non-'null' string here
      return (
        origin?.endsWith('.mycompany.com') || origin === 'http://localhost:3000'
      )
    },
  }),
)

Security behavior:

  • When an origin is not allowed, CORS headers (methods, allowed headers, credentials, exposed headers) are not set on the response. Only Vary: Origin is added.
  • null origins (from sandboxed iframes, file:// URLs, etc.) are rejected for array and function origin configurations.
  • Vary: Origin is set for all non-wildcard origin configurations to prevent CDN cache poisoning.
  • Wildcard (*) origins with credentials: true are blocked (CORS spec requirement).

TypeScript Usage:

import {createCORS} from '0http-bun/lib/middleware'
import type {CORSOptions} from '0http-bun/lib/middleware'

const corsOptions: CORSOptions = {
  origin: ['https://example.com', 'https://app.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true,
}

router.use(createCORS(corsOptions))

JWT Authentication

JSON Web Token authentication and authorization middleware with support for static secrets, JWKS endpoints, and API key authentication.

📦 Required dependency: bun install jose

Basic JWT with Static Secret

const {createJWTAuth} = require('0http-bun/lib/middleware')

// Basic JWT verification with static secret
router.use(
  '/api/protected/*',
  createJWTAuth({
    secret: 'your-secret-key',
    algorithms: ['HS256'],
  }),
)

TypeScript Usage:

import {createJWTAuth} from '0http-bun/lib/middleware'
import type {JWTAuthOptions} from '0http-bun/lib/middleware'

const jwtOptions: JWTAuthOptions = {
  secret: 'your-secret-key',
  jwtOptions: {
    algorithms: ['HS256'],
    audience: 'your-api',
    issuer: 'your-service',
  },
}

router.use('/api/protected/*', createJWTAuth(jwtOptions))

JWT with JWKS URI (Recommended for Production)

For production applications, especially when integrating with identity providers like Auth0, AWS Cognito, or Azure AD, use JWKS URI for automatic key rotation:

// Using JWKS URI (Auth0 example)
router.use(
  '/api/protected/*',
  createJWTAuth({
    jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json',
    algorithms: ['RS256'],
    issuer: 'https://your-domain.auth0.com/',
    audience: 'your-api-identifier',
  }),
)

// AWS Cognito example
router.use(
  '/api/protected/*',
  createJWTAuth({
    jwksUri:
      'https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json',
    algorithms: ['RS256'],
    issuer: 'https://cognito-idp.{region}.amazonaws.com/{userPoolId}',
    audience: 'your-client-id',
  }),
)

// Azure AD example
router.use(
  '/api/protected/*',
  createJWTAuth({
    jwksUri: 'https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys',
    algorithms: ['RS256'],
    issuer: 'https://login.microsoftonline.com/{tenant}/v2.0',
    audience: 'your-application-id',
  }),
)

// Google Identity example
router.use(
  '/api/protected/*',
  createJWTAuth({
    jwksUri: 'https://www.googleapis.com/oauth2/v3/certs',
    algorithms: ['RS256'],
    issuer: 'https://accounts.google.com',
    audience: 'your-client-id.apps.googleusercontent.com',
  }),
)

Advanced Configuration

// Complete configuration example
router.use(
  createJWTAuth({
    // Option 1: Static secret (for development/simple cases)
    secret: process.env.JWT_SECRET,

    // Option 2: JWKS URI (recommended for production)
    jwksUri: process.env.JWKS_URI,

    // JWT verification options
    // IMPORTANT: Do NOT mix symmetric (HS*) and asymmetric (RS*/ES*/PS*) algorithms.
    // Use HS256 with static secrets, or RS256/ES256 with JWKS URIs.
    algorithms: ['HS256'],
    issuer: 'your-app',
    audience: 'your-users',
    clockTolerance: 10, // Clock skew tolerance (seconds)
    ignoreExpiration: false,
    ignoreNotBefore: false,

    // Custom token extraction
    getToken: (req) => {
      // Try multiple sources
      return (
        req.headers.get('x-auth-token') ||
        req.headers.get('authorization')?.replace('Bearer ', '')
      )
    },

    // Alternative token sources
    tokenHeader: 'x-custom-token', // Custom header name
    tokenQuery: 'access_token', // Query parameter name (see security note below)

    // Error handling
    onError: (err, req) => {
      console.error('JWT Error:', err)
      return Response.json(
        {
          error: 'Unauthorized',
          code: err.name,
          message:
            process.env.NODE_ENV === 'development' ? err.message : undefined,
        },
        {status: 401},
      )
    },

    // Custom unauthorized response
    unauthorizedResponse: (error, req) => {
      return Response.json(
        {
          error: 'Access denied',
          requestId: req.headers.get('x-request-id'),
          timestamp: new Date().toISOString(),
        },
        {status: 401},
      )
    },

    // Optional authentication (proceed even without token)
    // When optional: true, invalid tokens set req.ctx.authError and req.ctx.authAttempted = true
    optional: false,

    // Exclude certain paths (uses exact match or path boundary, NOT prefix matching)
    // e.g., '/health' excludes '/health' and '/health/...' but NOT '/healthcheck'
    excludePaths: ['/health', '/metrics', '/api/public'],

    // Token type validation (validates the JWT 'typ' header claim)
    // Case-insensitive comparison. Rejects tokens with missing or incorrect type.
    requiredTokenType: 'JWT', // or 'at+jwt' for access tokens, etc.
  }),
)

Security Note on tokenQuery: Passing JWTs via query parameters exposes tokens in server logs, browser history, and HTTP Referer headers. Prefer header-based token extraction in production.

API Key Authentication

The JWT middleware also supports API key authentication as an alternative or fallback:

// API key with static keys
router.use(
  '/api/*',
  createJWTAuth({
    apiKeys: ['key1', 'key2', 'key3'],
    apiKeyHeader: 'x-api-key', // Default header
  }),
)

// Access the raw API key (req.apiKey is masked for security)
import {API_KEY_SYMBOL} from '0http-bun/lib/middleware/jwt-auth'

router.get('/api/data', (req) => {
  console.log(req.apiKey) // 'xxxx****xxxx' (masked, safe for logging)
  console.log(req[API_KEY_SYMBOL]) // Raw API key via Symbol.for('0http.apiKey')
})

// API key with custom validation
router.use(
  '/api/*',
  createJWTAuth({
    apiKeyValidator: async (apiKey, req) => {
      // Custom validation logic
      const user = await validateApiKeyInDatabase(apiKey)
      return user ? {id: user.id, name: user.name, apiKey} : false
    },
    apiKeyHeader: 'x-api-key',
  }),
)

// Combined JWT + API Key authentication
router.use(
  '/api/*',
  createJWTAuth({
    // JWT configuration
    jwksUri: process.env.JWKS_URI,
    algorithms: ['RS256'],

    // API Key fallback
    apiKeys: process.env.API_KEYS?.split(','),
    apiKeyHeader: 'x-api-key',

    // If JWT fails, try API key
    optional: false,
  }),
)

Environment-Based Configuration

// Dynamic configuration based on environment
const jwtConfig =
  process.env.NODE_ENV === 'production'
    ? {
        // Production: Use JWKS for security and key rotation
        jwksUri: process.env.JWKS_URI,
        algorithms: ['RS256'],
        issuer: process.env.JWT_ISSUER,
        audience: process.env.JWT_AUDIENCE,
      }
    : {
        // Development: Use static secret for simplicity
        secret: process.env.JWT_SECRET || 'dev-secret-key',
        algorithms: ['HS256'],
      }

router.use('/api/protected/*', createJWTAuth(jwtConfig))

Access Decoded Token Data

// Access decoded token in route handlers
router.get('/api/profile', (req) => {
  // Decoded JWT payload
  console.log(req.user) // Decoded JWT payload
  console.log(req.ctx.user) // Same as req.user

  // JWT header and payload (raw token is NOT included for security)
  console.log(req.jwt) // { payload, header }
  console.log(req.ctx.jwt) // Same as req.jwt

  // API key authentication data (if used)
  console.log(req.apiKey) // Masked API key (xxxx****xxxx)
  console.log(req.ctx.apiKey) // Same as req.apiKey (masked)

  // Optional mode: check if auth was attempted but failed
  if (req.ctx.authAttempted && req.ctx.authError) {
    console.log('Auth failed:', req.ctx.authError)
  }

  return Response.json({
    user: req.user,
    tokenInfo: {
      issuer: req.jwt?.payload.iss,
      audience: req.jwt?.payload.aud,
      expiresAt: new Date(req.jwt?.payload.exp * 1000),
      issuedAt: new Date(req.jwt?.payload.iat * 1000),
    },
  })
})

Logger

Request logging middleware with customizable output formats.

📦 Required dependency for structured logging: bun install pino
Simple logger (simpleLogger) has no dependencies - uses console.log

const {createLogger, simpleLogger} = require('0http-bun/lib/middleware')

// Simple logging
router.use(simpleLogger())

// Detailed logging with custom format
router.use(
  createLogger({
    pinoOptions: {
      level: 'info',
      transport: {
        target: 'pino-pretty',
        options: {colorize: true},
      },
    },
    logBody: false,
    excludePaths: ['/health', '/metrics'],
  }),
)

TypeScript Usage:

import {createLogger, simpleLogger} from '0http-bun/lib/middleware'
import type {LoggerOptions} from '0http-bun/lib/middleware'

const loggerOptions: LoggerOptions = {
  pinoOptions: {
    level: 'info',
    transport: {
      target: 'pino-pretty',
      options: {colorize: true},
    },
  },
  logBody: true,
  excludePaths: ['/health', '/ping'],
  serializers: {
    req: (req) => ({
      method: req.method,
      url: req.url,
      userAgent: req.headers.get('user-agent'),
    }),
  },
}

router.use(createLogger(loggerOptions))

Available Formats:

  • combined - Apache Combined Log Format
  • common - Apache Common Log Format
  • short - Shorter than common, includes response time
  • tiny - Minimal output
  • dev - Development-friendly colored output

Prometheus Metrics

Comprehensive Prometheus metrics integration for monitoring and observability with built-in security and performance optimizations.

📦 Required dependency: bun install prom-client

const {
  createPrometheusMiddleware,
  createMetricsHandler,
  createPrometheusIntegration,
} = require('0http-bun/lib/middleware')

// Simple setup with default metrics
const prometheus = createPrometheusIntegration()

router.use(prometheus.middleware)
router.get('/metrics', prometheus.metricsHandler)

Default Metrics Collected

The Prometheus middleware automatically collects:

  • HTTP Request Duration - Histogram of request durations in seconds (buckets: 0.001, 0.005, 0.015, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1, 2, 5, 10)
  • HTTP Request Count - Counter of total requests by method, route, and status
  • HTTP Request Size - Histogram of request body sizes (buckets: 1B, 10B, 100B, 1KB, 10KB, 100KB, 1MB, 10MB)
  • HTTP Response Size - Histogram of response body sizes (buckets: 1B, 10B, 100B, 1KB, 10KB, 100KB, 1MB, 10MB)
  • Active Connections - Gauge of currently active HTTP connections
  • Node.js Metrics - Memory usage, CPU, garbage collection (custom buckets), event loop lag (5ms precision)

Advanced Configuration

const {
  createPrometheusMiddleware,
  createMetricsHandler,
  createPrometheusIntegration,
} = require('0http-bun/lib/middleware')

const prometheus = createPrometheusIntegration({
  // Control default Node.js metrics collection
  collectDefaultMetrics: true,

  // Exclude paths from metrics collection (optimized for performance)
  excludePaths: ['/health', '/ping', '/favicon.ico'],

  // Skip certain HTTP methods
  skipMethods: ['OPTIONS'],

  // Custom route normalization with security controls
  normalizeRoute: (req) => {
    const url = new URL(req.url, 'http://localhost')
    return url.pathname
      .replace(/\/users\/\d+/, '/users/:id')
      .replace(/\/api\/v\d+/, '/api/:version')
  },

  // Add custom labels with automatic sanitization
  extractLabels: (req, response) => {
    return {
      user_type: req.headers.get('x-user-type') || 'anonymous',
      api_version: req.headers.get('x-api-version') || 'v1',
    }
  },

  // Use custom metrics object instead of default metrics
  metrics: customMetricsObject,
})

Custom Business Metrics

const {createPrometheusIntegration} = require('0http-bun/lib/middleware')

// Get the prometheus client from the integration
const prometheus = createPrometheusIntegration()
const {promClient} = prometheus

// Create custom metrics
const orderCounter = new promClient.Counter({
  name: 'orders_total',
  help: 'Total number of orders processed',
  labelNames: ['status', 'payment_method'],
})

const orderValue = new promClient.Histogram({
  name: 'order_value_dollars',
  help: 'Value of orders in dollars',
  labelNames: ['payment_method'],
  buckets: [10, 50, 100, 500, 1000, 5000],
})

// Use in your routes
router.post('/orders', async (req) => {
  const order = await processOrder(req.body)

  // Record custom metrics
  orderCounter.inc({
    status: order.status,
    payment_method: order.payment_method,
  })

  if (order.status === 'completed') {
    orderValue.observe(
      {
        payment_method: order.payment_method,
      },
      order.amount,
    )
  }

  return Response.json(order)
})

Metrics Endpoint Options

const {createMetricsHandler} = require('0http-bun/lib/middleware')

// Custom metrics endpoint
const metricsHandler = createMetricsHandler({
  endpoint: '/custom-metrics', // Default: '/metrics'
  registry: customRegistry, // Default: promClient.register
})

router.get('/custom-metrics', metricsHandler)

Route Normalization & Security

The middleware automatically normalizes routes and implements security measures to prevent high cardinality and potential attacks:

// URLs like these:
// /users/123, /users/456, /users/789
// Are normalized to: /users/:id

// /products/abc-123, /products/def-456
// Are normalized to: /products/:slug

// /api/v1/data, /api/v2/data
// Are normalized to: /api/:version/data

// Route sanitization examples:
// /users/:id → _users__id (special characters replaced with underscores)
// /api/v1/orders → _api_v1_orders
// Very long tokens → _api__token (pattern-based normalization)

Route Sanitization:

  • Special characters (/, :, etc.) are replaced with underscores (_) for Prometheus compatibility
  • UUIDs are automatically normalized to :id patterns
  • Long tokens (>20 characters) are normalized to :token patterns
  • Numeric IDs are normalized to :id patterns
  • Route complexity is limited to 10 segments maximum

Security Features:

  • Label Sanitization: Removes potentially dangerous characters from metric labels and truncates values to 100 characters
  • Cardinality Limits: Prevents memory exhaustion from too many unique metric combinations
  • Route Complexity Limits: Caps the number of route segments to 10 to prevent DoS attacks
  • Size Limits: Limits request/response body size processing (up to 100MB) to prevent memory issues
  • Header Processing Limits: Caps the number of headers processed per request (50 for requests, 20 for responses)
  • URL Processing: Handles both full URLs and pathname-only URLs with proper fallback handling

Performance Optimizations

  • Fast Path for Excluded Routes: Bypasses all metric collection for excluded paths with smart URL parsing
  • Lazy Evaluation: Only processes metrics when actually needed
  • Efficient Size Calculation: Optimized request/response size measurement with capping at 1MB estimation
  • Error Handling: Graceful handling of malformed URLs and invalid data with fallback mechanisms
  • Header Count Limits: Prevents excessive header processing overhead (50 request headers, 20 response headers)
  • Smart URL Parsing: Handles both full URLs and pathname-only URLs efficiently

Production Considerations

  • Performance: Adds <1ms overhead per request with optimized fast paths
  • Memory: Metrics stored in memory with cardinality controls; use recording rules for high cardinality
  • Security: Built-in protections against label injection and cardinality bombs
  • Cardinality: Automatic limits prevent high cardinality issues
  • Monitoring: Consider protecting /metrics endpoint in production

Integration with Monitoring

# prometheus.yml
scrape_configs:
  - job_name: '0http-bun-app'
    static_configs:
      - targets: ['localhost:3000']
    scrape_interval: 15s
    metrics_path: /metrics

Troubleshooting

Common Issues:

  • High Memory Usage: Check for high cardinality metrics. Route patterns should be normalized (e.g., /users/:id not /users/12345)
  • Missing Metrics: Ensure paths aren't in excludePaths and HTTP methods aren't in skipMethods
  • Route Sanitization: Routes are automatically sanitized (special characters become underscores: /users/:id_users__id)
  • URL Parsing Errors: The middleware handles both full URLs and pathname-only URLs with graceful fallback

Performance Tips:

  • Use excludePaths for health checks and static assets
  • Consider using skipMethods for OPTIONS requests
  • Monitor memory usage in production for metric cardinality
  • Use Prometheus recording rules for high-cardinality aggregations

Rate Limiting

Configurable rate limiting middleware with multiple store options.

No additional dependencies required - Uses built-in memory store.

const {createRateLimit, MemoryStore} = require('0http-bun/lib/middleware')

// Basic rate limiting
router.use(
  createRateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // Max 100 requests per windowMs
  }),
)

// Advanced configuration
router.use(
  createRateLimit({
    windowMs: 60 * 1000, // 1 minute
    max: 20, // Max requests
    keyGenerator: (req) => {
      // Custom key generation
      // Default uses: req.ip || req.remoteAddress || req.socket?.remoteAddress || 'unknown'
      // NOTE: Proxy headers are NOT trusted by default. If behind a
      // reverse proxy, you MUST provide a custom keyGenerator:
      return (
        req.headers.get('x-user-id') ||
        req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
        req.ip ||
        'unknown'
      )
    },
    skip: (req) => {
      // Skip rate limiting for certain requests
      return req.url.startsWith('/health')
    },
    handler: (req, totalHits, max, resetTime) => {
      // Custom rate limit exceeded response
      return Response.json(
        {
          error: 'Rate limit exceeded',
          resetTime: resetTime.toISOString(),
          retryAfter: Math.ceil((resetTime.getTime() - Date.now()) / 1000),
        },
        {status: 429},
      )
    },
    standardHeaders: true, // Send X-RateLimit-* headers
    // standardHeaders options:
    //   true     - full headers (X-RateLimit-Limit, Remaining, Reset, Used) — default
    //   false    - no rate limit headers
    //   'minimal' - only Retry-After on 429 responses (hides exact config/counters)
    // excludePaths uses exact match or boundary matching (NOT prefix)
    // e.g., '/health' matches '/health' and '/health/...' but NOT '/healthcheck'
    excludePaths: ['/health', '/metrics'],
  }),
)

// Custom store (for distributed systems)
router.use(
  createRateLimit({
    store: new MemoryStore(), // Built-in memory store
    // Or implement custom store with increment() method
    // Note: The store is always the constructor-configured instance.
    // It cannot be overridden at runtime via the request object.
  }),
)

TypeScript Usage:

import {createRateLimit, MemoryStore} from '0http-bun/lib/middleware'
import type {RateLimitOptions, RateLimitStore} from '0http-bun/lib/middleware'

const rateLimitOptions: RateLimitOptions = {
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  keyGenerator: (req) => {
    // Default: req.ip || req.remoteAddress || req.socket?.remoteAddress || 'unknown'
    // Custom: read from proxy-set header if behind a reverse proxy
    return (
      req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
      req.ip ||
      'unknown'
    )
  },
  standardHeaders: true,
  excludePaths: ['/health', '/ping'],
}

router.use(createRateLimit(rateLimitOptions))

// Custom store implementation
class CustomStore implements RateLimitStore {
  // Note: For MemoryStore, increment is synchronous to prevent TOCTOU races.
  // Custom stores may be async if using external backends (e.g., Redis).
  async increment(
    key: string,
    windowMs: number,
  ): Promise<{totalHits: number; resetTime: Date}> {
    // Custom implementation
    return {totalHits: 1, resetTime: new Date(Date.now() + windowMs)}
  }
}

Sliding Window Rate Limiter

For more precise rate limiting, use the sliding window implementation that prevents burst traffic at any point in time:

const {createSlidingWindowRateLimit} = require('0http-bun/lib/middleware')

// Basic sliding window rate limiter
router.use(
  createSlidingWindowRateLimit({
    windowMs: 60 * 1000, // 1 minute sliding window
    max: 10, // Max 10 requests per minute
    maxKeys: 10000, // Maximum tracked keys (default: 10000) — prevents unbounded memory growth
    keyGenerator: (req) =>
      req.ip || req.remoteAddress || req.socket?.remoteAddress || 'unknown',
  }),
)

Memory safety: The sliding window rate limiter enforces a maxKeys limit (default: 10,000). When the limit is exceeded, the oldest entry is evicted. A periodic cleanup interval runs at most every 60 seconds (or windowMs, whichever is shorter) and uses unref() so it doesn't prevent process exit.

TypeScript Usage:

import {createSlidingWindowRateLimit} from '0http-bun/lib/middleware'
import type {RateLimitOptions} from '0http-bun/lib/middleware'

const slidingOptions: RateLimitOptions = {
  windowMs: 60 * 1000, // 1 minute
  max: 10, // 10 requests max
  maxKeys: 10000, // Max tracked keys (default: 10000)
  keyGenerator: (req) => req.user?.id || req.ip || 'unknown',
  handler: (req, hits, max, resetTime) => {
    return Response.json(
      {
        error: 'Rate limit exceeded',
        retryAfter: Math.ceil((resetTime.getTime() - Date.now()) / 1000),
        limit: max,
        used: hits,
      },
      {status: 429},
    )
  },
}

router.use(createSlidingWindowRateLimit(slidingOptions))

How Sliding Window Differs from Fixed Window:

The sliding window approach provides more accurate and fair rate limiting by tracking individual request timestamps:

  • Fixed Window: Divides time into discrete chunks (e.g., 09:00:00-09:00:59, 09:01:00-09:01:59)
    • ⚠️ Problem: Allows burst traffic at window boundaries (20 requests in 2 seconds)
  • Sliding Window: Uses a continuous, moving time window from current moment
    • Advantage: Prevents bursts at any point in time (true rate limiting)

Use Cases for Sliding Window:

// Financial API - Zero tolerance for payment bursts
router.use(
  '/api/payments/*',
  createSlidingWindowRateLimit({
    windowMs: 60 * 1000, // 1 minute
    max: 3, // Only 3 payment attempts per minute
    keyGenerator: (req) => req.user.accountId,
  }),
)

// User Registration - Prevent automated signups
router.use(
  '/api/register',
  createSlidingWindowRateLimit({
    windowMs: 3600 * 1000, // 1 hour
    max: 3, // 3 accounts per IP per hour
    keyGenerator: (req) =>
      req.ip || req.remoteAddress || req.socket?.remoteAddress || 'unknown',
  }),
)

// File Upload - Prevent abuse
router.use(
  '/api/upload',
  createSlidingWindowRateLimit({
    windowMs: 300 * 1000, // 5 minutes
    max: 10, // 10 uploads per 5 minutes
    keyGenerator: (req) => req.user.id,
  }),
)

Performance Considerations:

  • Memory Usage: Higher than fixed window (stores timestamp arrays), bounded by maxKeys (default: 10,000)
  • Time Complexity: O(n) per request where n = requests in window
  • Cleanup: Automatic periodic cleanup runs at most every 60 seconds; interval uses unref() to not prevent process exit
  • Best For: Critical APIs, financial transactions, user-facing features
  • Use Fixed Window For: High-volume APIs where approximate limiting is acceptable

Advanced Configuration:

// Tiered rate limiting based on user level
const createTieredRateLimit = (req) => {
  const userTier = req.user?.tier || 'free'
  const configs = {
    free: {windowMs: 60 * 1000, max: 10},
    premium: {windowMs: 60 * 1000, max: 100},
    enterprise: {windowMs: 60 * 1000, max: 1000},
  }
  return createSlidingWindowRateLimit(configs[userTier])
}

Rate Limit Headers:

Both rate limiters send the following headers when standardHeaders: true:

  • X-RateLimit-Limit - Request limit
  • X-RateLimit-Remaining - Remaining requests
  • X-RateLimit-Reset - Reset time (Unix timestamp)
  • X-RateLimit-Used - Used requests

When standardHeaders: 'minimal', only Retry-After is sent on 429 responses. This prevents disclosing exact rate limit configuration and usage counters to clients.

Error Handling:

Rate limiting middleware allows errors to bubble up as proper HTTP 500 responses. If your keyGenerator function or custom store.increment() method throws an error, it will not be caught and masked - the error will propagate up the middleware chain for proper error handling.

Creating Custom Middleware

Basic Middleware

import {ZeroRequest, StepFunction} from '0http-bun'

const customMiddleware = (req: ZeroRequest, next: StepFunction) => {
  // Pre-processing
  req.ctx = req.ctx || {}
  req.ctx.startTime = Date.now()

  // Continue to next middleware/handler
  const response = next()

  // Post-processing (if needed)
  return response
}

router.use(customMiddleware)

Async Middleware

const asyncMiddleware = async (req: ZeroRequest, next: StepFunction) => {
  // Async pre-processing
  const user = await validateUserSession(req)
  req.ctx = {user}

  // Continue
  const response = await next()

  // Async post-processing
  await logUserActivity(user, req.url)

  return response
}

Error Handling in Middleware

const errorHandlingMiddleware = async (
  req: ZeroRequest,
  next: StepFunction,
) => {
  try {
    return await next()
  } catch (error) {
    console.error('Middleware error:', error)

    // Return error response
    return Response.json(
      {
        error: 'Internal server error',
        message:
          process.env.NODE_ENV === 'development' ? error.message : undefined,
      },
      {status: 500},
    )
  }
}

Middleware Execution Order

Middlewares execute in the order they are registered:

router.use(middleware1) // Executes first
router.use(middleware2) // Executes second
router.use(middleware3) // Executes third

router.get('/test', handler) // Final handler

Path-Specific Middleware

Apply middleware only to specific paths:

// API-only middleware
router.use('/api/*', createJWTAuth({secret: 'api-secret'}))
router.use('/api/*', createRateLimit({max: 1000}))

// Admin-only middleware
router.use('/admin/*', adminAuthMiddleware)
router.use('/admin/*', auditLogMiddleware)

// Public paths (no auth required)
router.get('/health', healthCheckHandler)
router.get('/metrics', metricsHandler)

Best Practices

  1. Order Matters: Place security middleware (CORS, auth) before business logic
  2. Error Handling: Always handle errors in async middleware
  3. Performance: Use skip functions to avoid unnecessary processing
  4. Context: Use req.ctx to pass data between middlewares
  5. Immutability: Don't modify the original request object directly
  6. Logging: Log middleware errors for debugging
  7. Testing: Test middleware in isolation with mock requests

Examples

Complete Middleware Stack

const {
  createCORS,
  createLogger,
  createBodyParser,
  createJWTAuth,
  createRateLimit,
} = require('0http-bun/lib/middleware')

const router = http()

// 1. CORS (handle preflight requests first)
router.use(
  createCORS({
    origin: process.env.ALLOWED_ORIGINS?.split(','),
    credentials: true,
  }),
)

// 2. Logging (log all requests)
router.use(createLogger({format: 'combined'}))

// 3. Rate limiting (protect against abuse)
// NOTE: Default key generator uses req.ip — provide keyGenerator if behind a proxy
router.use(
  createRateLimit({
    windowMs: 15 * 60 * 1000,
    max: 1000,
    // keyGenerator: (req) => req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || req.ip || 'unknown',
  }),
)

// 4. Body parsing (parse request bodies)
router.use(createBodyParser({limit: '10mb'}))

// 5. Authentication (protect API routes)
// Default algorithms: ['HS256'] — specify ['RS256'] for asymmetric keys
router.use(
  '/api/*',
  createJWTAuth({
    secret: process.env.JWT_SECRET,
  }),
)

// Routes
router.get('/api/public/status', () => Response.json({status: 'ok'}))
router.get('/api/protected/data', (req) => Response.json({user: req.user}))

Dependency Summary

For your convenience, here's a quick reference of which dependencies you need to install for each middleware:

Middleware Dependencies Required Install Command
Body Parser ✅ None Built-in
CORS ✅ None Built-in
Rate Limiting ✅ None Built-in
Logger (simple) ✅ None Built-in
Logger (structured) 📦 pino bun install pino
JWT Authentication 📦 jose bun install jose
Prometheus Metrics 📦 prom-client bun install prom-client

Install all optional dependencies at once:

bun install pino jose prom-client

This middleware stack provides a solid foundation for most web applications with security, logging, and performance features built-in.