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:
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.
function assert<T>(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
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
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.
function assertValidation<T>(
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
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.
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.
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 |
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
@RateLimitdirectly beneath the route decorator (@Get,@Post, …). The route decorator captures the handler's parameters when it is applied, so a@RateLimitabove it would be ignored — this is detected at startup and throws, rather than silently leaving the route unprotected.
Limit by client IP
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
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).