Client SDK#
Type-safe RPC client for calling your API from the frontend or other services. Define route contracts once, get full type inference everywhere.
Imports#
typescript
import { createClient } from "@buntok/core/client";
import type { RouteContract } from "@buntok/core/client";Define Contracts#
Each route is declared as a RouteContract<Params, Query, Body, Response>:
typescript
const routes = {
getUser: {
method: "GET",
path: "/users/:id",
} as RouteContract<
{ id: string }, // params
undefined, // query
undefined, // body
{ id: string; name: string } // response
>,
listUsers: {
method: "GET",
path: "/users",
} as RouteContract<
undefined,
{ page?: number; limit?: number }, // query
undefined,
{ data: { id: string; name: string }[]; total: number }
>,
createUser: {
method: "POST",
path: "/users",
} as RouteContract<
undefined,
undefined,
{ name: string; email: string }, // body
{ id: string; name: string }
>,
};Create Client#
typescript
const api = createClient(routes, "http://localhost:1212");
// Fully typed — params, body, and return type are all inferred
const user = await api.getUser({ params: { id: "1" } });
// user: { id: string; name: string }
const list = await api.listUsers({ query: { page: 2, limit: 10 } });
// list: { data: { id: string; name: string }[]; total: number }
const created = await api.createUser({
body: { name: "Tok", email: "tok@example.com" },
});
// created: { id: string; name: string }Options#
| Option | Type | Default | Description |
|---|---|---|---|
| headers | Record<string, string> | — | Headers sent on every request |
| timeout | number | 30000 | Request timeout in ms |
| retries | number | 0 | Number of retry attempts |
| retryDelay | number | 1000 | Delay between retries in ms |
| retryOn | number[] | [408, 429, 500, 502, 503, 504] | Status codes to retry on |
| onRequest | (req) => Request | — | Request interceptor |
| onResponse | (res) => Response | — | Response interceptor |
| fetch | typeof fetch | globalThis.fetch | Override fetch (useful for testing) |
ClientError#
Typed error thrown when a request fails (non-2xx status or network error):
typescript
import { ClientError } from "@buntok/core/client";
try {
await api.getUser({ params: { id: "999" } });
} catch (err) {
if (err instanceof ClientError) {
console.log(err.status); // 404
console.log(err.method); // "GET"
console.log(err.path); // "/users/999"
console.log(err.body); // response body (string or null)
console.log(err.message); // "GET /users/999 failed with status 404"
}
}How It Works#
createClient()iterates over contract keys and creates typed functions- Each function:
- Substitutes
:paramsin the path (e.g./users/:id→/users/1) - Appends
?queryparams to URL - Serializes
bodyas JSON - Adds
Content-Type: application/jsonheader if body present - Applies
onRequestinterceptor before fetch - Applies
onResponseinterceptor after fetch - Retries on failure if
retries > 0and status matchesretryOn - Parses response as JSON or text based on Content-Type
- Throws
ClientErroron non-2xx if not retryable
RouteContract Type#
typescript
interface RouteContract<
TParams = undefined, // URL params (e.g. { id: string })
TQuery = undefined, // Query params (e.g. { page?: number })
TBody = undefined, // Request body (e.g. { name: string })
TResponse = unknown, // Response type
> {
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
path: string; // e.g. "/users/:id"
params?: TParams; // type-only, never read at runtime
query?: TQuery;
body?: TBody;
response?: TResponse;
}Info
The
RouteContract type is type-only — params, query, body, and response fields are never read at runtime. They only exist for TypeScript inference.