# Interface API Util Documentation ## Overview Interface API Util provides utilities for validating API requests, rate limiting handlers, and returning structured HTTP error responses. The package exports `assert` for checking conditions, `assertValidation` for running input validators, and the `@RateLimit` decorator for throttling handlers. All of them surface failures as `HTTPResult` errors from `@antelopejs/interface-api`. ## Getting Started Install the package in your project: ```bash npm install @antelopejs/interface-api-util ``` ## `assert` The `assert` function checks whether a condition is truthy. If the condition is falsy, it throws an `HTTPResult` with the specified HTTP status code and error message. ```typescript function assert(condition: T, code: number, message: string): asserts condition; ``` | Parameter | Type | Description | | ----------- | -------- | ---------------------------------------------------- | | `condition` | `T` | The value to check (falsy values trigger the error) | | `code` | `number` | HTTP status code for the error response | | `message` | `string` | Error message included in the response body | The function uses TypeScript's `asserts` keyword, so the compiler narrows the type of `condition` to truthy after the call. ### Check that a resource exists ```typescript import { assert } from "@antelopejs/interface-api-util"; import { HTTPResult } from "@antelopejs/interface-api"; async function getUser(id: string) { const user = await findUser(id); assert(user, 404, "User not found"); // TypeScript knows `user` is defined here return new HTTPResult(200, user); } ``` ### Validate permissions ```typescript import { assert } from "@antelopejs/interface-api-util"; import { HTTPResult } from "@antelopejs/interface-api"; async function deletePost(postId: string, currentUserId: string) { const post = await findPost(postId); assert(post, 404, "Post not found"); assert(post.authorId === currentUserId, 403, "Not authorized to delete this post"); await removePost(postId); return new HTTPResult(200, { deleted: true }); } ``` ## `assertValidation` The `assertValidation` function runs a validator on an input value. If the validator throws, `assertValidation` catches the error and throws an `HTTPResult` instead. ```typescript function assertValidation( body: unknown, validator: (body: unknown) => T, errorFunc?: (err: unknown) => unknown, code?: number, ): T; ``` | Parameter | Type | Default | Description | | ----------- | ----------------------------- | ------- | ------------------------------------------------------- | | `body` | `unknown` | | The input value to validate | | `validator` | `(body: unknown) => T` | | A function that returns the validated value or throws | | `errorFunc` | `(err: unknown) => unknown` | | Optional error transformer for customizing the response | | `code` | `number` | `400` | HTTP status code for the error response | The function returns the validated value on success. On failure, it throws an `HTTPResult` whose body is either the result of `errorFunc` or the stringified error. ### Validate with Zod ```typescript import { assertValidation } from "@antelopejs/interface-api-util"; import * as z from "zod"; const createUserSchema = z.object({ name: z.string().min(1), email: z.string().email(), }); function createUser(requestBody: unknown) { const { name, email } = assertValidation(requestBody, createUserSchema.parse); // `name` and `email` are typed strings here return saveUser({ name, email }); } ``` ### Custom error formatting Pass an `errorFunc` to control the error response body. ```typescript import { assertValidation } from "@antelopejs/interface-api-util"; import * as z from "zod"; const schema = z.object({ age: z.number().min(18) }); function validateAge(requestBody: unknown) { return assertValidation( requestBody, schema.parse, (err) => { if (err instanceof z.ZodError) { return { errors: err.errors.map((e) => e.message) }; } return { errors: [String(err)] }; }, 422, ); } ``` ## `RateLimit` The `@RateLimit` decorator caps how often a route handler can be called. It counts requests per client in a fixed time window (in-memory, per process). Successful responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (a UNIX epoch-seconds timestamp) so clients can self-pace. Once the count exceeds `limit` within `windowMs`, the handler instead throws an `HTTPResult` with status `429` (adding a `Retry-After` header) without running. ```typescript function RateLimit( limit: number, windowMs: number, options?: RateLimitOptions, ): MethodDecorator; ``` | Parameter | Type | Description | | ---------- | ------------------ | -------------------------------------------------------- | | `limit` | `number` | Maximum number of requests allowed per window | | `windowMs` | `number` | Window duration in milliseconds | | `options` | `RateLimitOptions` | Optional custom key function and `429` response body | ```typescript interface RateLimitOptions { key?: (context: RequestContext) => string | null | undefined; message?: string; } ``` By default, requests are counted per client IP (`context.rawRequest.socket.remoteAddress`). Provide `options.key` to count by user id, API key, or a forwarded header instead. If the custom key function returns a falsy value (for example a missing route parameter), the limiter falls back to the client IP rather than collapsing every such request into one shared bucket. > **Ordering matters.** Place `@RateLimit` **directly beneath** the route decorator (`@Get`, `@Post`, …). The route decorator captures the handler's parameters when it is applied, so a `@RateLimit` above it would be ignored — this is detected at startup and throws, rather than silently leaving the route unprotected. ### Limit by client IP ```typescript import { RateLimit } from "@antelopejs/interface-api-util"; import { Controller, Get, HTTPResult } from "@antelopejs/interface-api"; class UsersController extends Controller("users") { @Get() @RateLimit(100, 60_000) list() { return new HTTPResult(200, { users: [] }); } } ``` ### Limit by a custom key ```typescript import { RateLimit } from "@antelopejs/interface-api-util"; import { Controller, Post, HTTPResult } from "@antelopejs/interface-api"; class AuthController extends Controller("auth") { @Post("login") @RateLimit(5, 60_000, { key: (ctx) => ctx.routeParameters.tenant, message: "Too many login attempts, slow down", }) login() { return new HTTPResult(200, { ok: true }); } } ``` Counts are kept in process memory, so they are not shared across instances and reset on restart. Elapsed windows are pruned periodically, so the store is bounded by the number of distinct keys seen within a single window rather than growing forever. For distributed limits, key by a stable identifier and enforce limits in a shared store at the edge. Behind a proxy, the socket address is the proxy's — supply `options.key` to read a forwarded header, but only trust the hop your proxy sets (a raw client-supplied `X-Forwarded-For` is spoofable and can be used to evade or to inflate the key cardinality).