TypeDrop

2026-07-01 Challenge

2026-07-01 Medium

Typed API Rate Limiter with Sliding Window & Per-Client Policies

You're building the rate-limiting middleware for a multi-tenant REST API gateway. Raw inbound requests arrive as `unknown` from an HTTP adapter; your module must validate them into strongly-typed request descriptors, look up per-client policies from a typed registry, apply a sliding-window algorithm to track usage, and return a fully-typed allow/deny decision — with zero `any`.

Goals

  • Implement `validateRequest` to parse `unknown` input into a `RequestDescriptor`, returning typed `ValidationError` variants for each failure case.
  • Implement `lookupPolicy` to resolve a clientId against a `PolicyRegistry`, returning an `UnknownClientError` when absent.
  • Implement `evaluateRateLimit` to apply a sliding-window counter using a mutable `WindowStore`, returning the correct `AllowedDecision` or `DeniedDecision`.
  • Wire all three steps together in `processRequest` and implement the `isAllowed` type-guard so callers can safely narrow the decision union.
challenge.ts

export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
export type ClientTier = "free" | "pro" | "enterprise";

export interface RequestDescriptor {
  clientId: string;
  method: HttpMethod;
  path: string;
  receivedAt: number; // Unix ms
}

export interface RateLimitPolicy {
  clientId: string;
  tier: ClientTier;
  windowMs: number;
  maxRequests: number;
}

export type RateLimitDecision =
  | { status: "allowed"; clientId: string; remaining: number; resetAt: number }
  | { status: "denied";  clientId: string; retryAfterMs: number };

export type Result<T, E> =
  | { ok: true;  value: T }
  | { ok: false; error: E };

export type WindowStore = Map<string, number[]>; // clientId → sorted timestamps

export function validateRequest(raw: unknown): Result<RequestDescriptor, ValidationError>;
export function evaluateRateLimit(req: RequestDescriptor, policy: RateLimitPolicy, store: WindowStore): RateLimitDecision;
export function isAllowed(decision: RateLimitDecision): decision is AllowedDecision;
Hints (click to reveal)

Hints

  • A const array like `const METHODS = ['GET','POST',...] as const` lets you derive `HttpMethod` and write a type-safe `.includes()` check without `any` — look into using a type predicate or casting the array to `ReadonlyArray<HttpMethod>`.
  • For the sliding window, `Array.prototype.filter` returns a new array — remember requirement [R11] says you must mutate the Map entry in-place, so reassign `store.set(clientId, prunedArray)` rather than creating a new Map.
  • The `Result<T, E>` union requires you to check `.ok` before TypeScript will let you access `.value` or `.error` — lean on that narrowing inside `processRequest` to propagate errors without type assertions.

Or clone locally

git clone -b challenge/2026-07-01 https://github.com/niltonheck/typedrop.git