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.
Useful resources
Or clone locally
git clone -b challenge/2026-07-01 https://github.com/niltonheck/typedrop.git