Client
createClient takes a contract and returns a fully typed API client. Each endpoint in the contract becomes a callable function on the client.
Creating a Client
import { createClient } from 'sorbus';
import { contract } from './contract';
export const api = createClient(contract, '/api');The second argument is the base URL. It can be a relative path (/api) or an absolute URL (https://api.example.com).
Options
The third argument is an options object:
export const api = createClient(contract, '/api', {
headers: {
Authorization: `Bearer ${token}`,
},
serializeKey: 'snake',
normalizeKey: 'camel',
cache: myCache,
});| Option | Type | Default | Description |
|---|---|---|---|
headers | HeadersInit | () => HeadersInit | — | Headers sent with every request |
serializeKey | 'camel' | 'pascal' | 'kebab' | 'snake' | (key: string) => string | — | Transform keys before sending |
normalizeKey | 'camel' | 'pascal' | 'kebab' | 'snake' | (key: string) => string | — | Transform keys after receiving |
fetch | typeof fetch | globalThis.fetch | Custom fetch implementation |
cache | Cache | — | ETag cache for GET requests (see Caching) |
See Key Transforms for details.
Headers
Pass headers as an object for static values like API keys:
const api = createClient(contract, '/api', {
headers: {
Authorization: `Bearer ${token}`,
'X-API-Key': apiKey,
},
});Pass a function for dynamic values like tokens that change over time:
const api = createClient(contract, '/api', {
headers: () => ({
Authorization: `Bearer ${getToken()}`,
}),
});Per-Request Options
Every endpoint call accepts an optional second argument with headers and signal. Per-request headers are merged with global headers — per-request values take precedence on collision.
const { invoice } = await api.invoices.show({ id: '123' }, {
headers: {
'X-Trace-Id': traceId,
},
signal: abortController.signal,
});signal is useful for cancelling requests on navigation — for example in React or SvelteKit remote functions:
const { invoices } = await api.invoices.index({ page: 1 }, {
signal: controller.signal,
});Per-request options work with catch and .raw():
const result = await api.invoices.create(
{
invoice: {
number: 'INV-001',
},
},
{
catch: [422],
signal,
},
);
await api.invoices.update.raw(
{
pathParams: {
id: '456',
},
body: {
invoice: {
total: 1500,
},
},
},
{ signal },
);Custom Fetch
For advanced cases — credentials, logging, interceptors — pass a custom fetch:
const api = createClient(contract, '/api', {
fetch: (url, init) => {
return fetch(url, {
...init,
credentials: 'include',
});
},
});Type Inference
The client's type is fully inferred from the contract. Endpoint nesting, params, and response types all flow from the contract definition:
// Contract nesting becomes client nesting
api.invoices.index(...)
api.accounts.timeEntries.create(...)
// Params are inferred from pathParams + request.query + request.body
api.invoices.show({ id: '123' })
// ^? { id: string }
// Response is inferred from the response schema
const { invoice } = await api.invoices.show({ id: '123' });
// ^? { id: string; number: string; total: number; state: "draft" | "sent" | "paid" }The error type (TError) is inferred from the contract's error schema. It appears as the data field on caught errors. See Error Handling.