Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
"dist",
"docs",
"skills"
],
"exports": {
".": {
Expand All @@ -36,7 +38,10 @@
"test": "pnpm run build && ajs module test ."
},
"antelopeJs": {
"test": "src/antelope.test.ts"
"test": "src/antelope.test.ts",
"skills": [
"./skills"
]
},
"devDependencies": {
"@antelopejs/interface-api": "^0.0.5",
Expand Down
81 changes: 81 additions & 0 deletions skills/api-util-interface/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
name: api-util-interface
description: Provides handler-side utilities for AntelopeJS APIs built on @antelopejs/interface-api - assert and assertValidation turn failed checks into HTTPResult error responses, and the @RateLimit method decorator adds fixed-window throttling with 429/Retry-After and X-RateLimit headers. Use when code imports from "@antelopejs/interface-api-util", when validating request bodies (e.g. with a Zod schema) inside @Get/@Post handlers, when returning 400/404/403-style errors from API controllers, or when asked to rate limit a route.
category: antelopejs-interface
tags: [antelopejs, api, validation, rate-limit, http]
---

# API Util Interface

Consumer-side helper library for AntelopeJS API modules. It is NOT an interface proxy itself:
nothing here crosses an interface boundary, so there is no `ImplementInterface` side. Everything
builds on its peer dependencies `@antelopejs/interface-api` (for `HTTPResult`, `RequestContext`,
controller metadata) and `@antelopejs/interface-core` (decorator machinery). All failures surface
as thrown `HTTPResult` values, which the API interface converts into HTTP responses.

## Imports

The package has a single export subpath (root only):

```ts
import { assert, assertValidation, RateLimit, type RateLimitOptions } from "@antelopejs/interface-api-util";
```

`hitWindow`, `pruneExpired`, and `resolveKey` are also exported but marked `@internal` (rate-limit
plumbing, exposed for tests) — do not reach for them in application code.

## Usage

```ts
import { Controller, Get, Post, HTTPResult, Parameter, RawBody } from "@antelopejs/interface-api";
import { assert, assertValidation, RateLimit } from "@antelopejs/interface-api-util";
import * as z from "zod";

const createMemberSchema = z.object({ fullName: z.string().min(1), email: z.string().email() });

class MembersController extends Controller("members") {
@Get(":id")
@RateLimit(100, 60_000)
async get(@Parameter("id") id: string) {
const member = await findMember(id);
assert(member, 404, "Member not found"); // narrows `member` to non-falsy below
return new HTTPResult(200, member);
}

@Post()
async create(@RawBody() body: Buffer) {
const { fullName, email } = assertValidation(body, (b) => createMemberSchema.parse(JSON.parse((b as Buffer).toString())));
return new HTTPResult(201, await saveMember({ fullName, email }));
}
}
```

- `assert(condition, code, message)` — throws `new HTTPResult(code, message)` when falsy; uses
TypeScript `asserts condition`, so the compiler narrows after the call.
- `assertValidation(body, validator, errorFunc?, code = 400)` — returns the validator's result;
if the validator throws, rethrows an `HTTPResult` whose body is `errorFunc(err)` if provided,
otherwise `String(err)`.
- `RateLimit(limit, windowMs, options?)` — fixed-window counter per key; over-limit requests throw
a 429 `HTTPResult` (with `Retry-After`) before the handler runs. Successful responses get
`X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (epoch seconds) headers.

## Gotchas

- Decorator ordering: `@RateLimit` must sit directly BENEATH the route decorator
(`@Get`, `@Post`, ...). Placed above it, the guard would be ignored — this is detected at
decoration time and throws an `Error` at startup instead of silently unprotecting the route.
- Rate-limit keys default to the client IP (`context.rawRequest.socket.remoteAddress`). Behind a
proxy that is the proxy's address — supply `options.key` reading a trusted forwarded header. If
the custom `key` function returns a falsy value, the limiter falls back to the client IP rather
than merging those requests into one shared bucket.
- Counts live in process memory: not shared across instances, reset on restart, pruned once the
window elapses. For distributed limits, enforce them in a shared store at the edge.
- `options.message` customizes only the 429 body; the status code is fixed at 429.
- These helpers only throw `HTTPResult`; they do not send responses themselves — they rely on the
API interface's handling of thrown `HTTPResult` values.

## Deeper reference

See this package's `docs/1.introduction.md` (Interface API Util Documentation: `assert`,
`assertValidation`, `RateLimit` sections) and the shipped `dist/*.d.ts` typings. Do not duplicate
them here.
Loading