Files
2026-05-27 23:47:53 +01:00

3.8 KiB

rate-limit-middleware

(no submodules)

Purpose

Provides framework-agnostic middleware factory helpers that wrap createRateLimiter for easy integration into HTTP servers, API gateways, and custom request pipelines. All middleware functions accept req: unknown and return a Promise<RateLimitResult>, keeping them decoupled from any specific framework (Hono, Express, Fastify, Bun's native server, etc.).

The three exported helpers cover the most common patterns: a simple checker function, IP-based limiting, and route-specific per-path limits. For Hono-specific wiring, consumers can use a thin adapter that reads result.allowed and returns a 429 response.

Features

  • createRateLimitChecker — returns a (context) => Promise<RateLimitResult> wrapper
  • getRateLimitResult — low-level direct storage+algorithm call
  • createIpRateLimiter — IP-key extraction with configurable keyExtractor, skip, onLimited
  • createRouteRateLimiter — per-route config map with route-prefix matching
  • All factories accept an optional skip predicate to bypass limiting for health probes
  • onLimited callback for telemetry, logging, or 429-response generation

Usage: Basic Example

import { createRateLimitChecker } from "@suaveplan/rate-limiter";

const check = createRateLimitChecker({
  defaultConfig: { limit: 100, window: 60_000 },
});

const result = await check({ key: "user:42" });
if (!result.allowed) {
  return new Response("Too Many Requests", { status: 429 });
}

Usage: Advanced Example

import { createIpRateLimiter, createRouteRateLimiter, MemoryStorage } from "@suaveplan/rate-limiter";

const storage = new MemoryStorage({ maxSize: 10_000 });

// IP-based limiter that skips health checks
const ipLimiter = createIpRateLimiter(
  { limit: 200, window: 60_000 },
  storage,
  {
    keyExtractor: (req) => (req as Request).headers.get("x-forwarded-for") ?? "unknown",
    skip: (req) => (req as Request).url.endsWith("/health"),
    onLimited: (result) => console.warn("IP limited", result),
  }
);

// Route-specific limits
const routeLimiter = createRouteRateLimiter(
  {
    "/api/auth/login": { limit: 5, window: 60_000 },
    "/api/search": { limit: 50, window: 10_000 },
  },
  storage
);

const result = await ipLimiter(request);
if (!result.allowed) {
  return new Response("Too Many Requests", { status: 429 });
}

API Reference

createRateLimitChecker(options)

(options: RateLimiterOptions) => (context: RateLimitContext) => Promise<RateLimitResult>

getRateLimitResult(context, storage, config)

(context: RateLimitContext, storage: RateLimitStorage, config: RateLimitConfig) => Promise<RateLimitResult>

Low-level helper — creates a fresh SlidingWindow per call, restoring persisted state from storage.

createIpRateLimiter(config, storage?, middlewareOptions?)

Returns (req: unknown) => Promise<RateLimitResult>.

createRouteRateLimiter(routes, storage?, middlewareOptions?)

Returns (req: unknown) => Promise<RateLimitResult>. Matches route prefixes in insertion order.

RateLimiterMiddlewareOptions

Field Type Description
keyExtractor? (req: unknown) => string Extract key from request
onLimited? (result: RateLimitResult) => void Called on every check (use to send 429)
skip? (req: unknown) => boolean Skip limiting for matching requests

Implementation Notes

createIpRateLimiter and createRouteRateLimiter each construct one createRateLimiter instance per route/IP config at factory time, sharing a single storage adapter. Route matching in createRouteRateLimiter uses key.startsWith(route) || key.includes(route) — for production use with complex routing rules, consumers should provide a custom keyExtractor that returns the matched route prefix.