@auldrant/api is a client-side library that makes REST API calls simple and correct. It wraps the Fetch API with ergonomic method helpers, automatic JSON serialization, and a discriminated union response type so you always know whether a request succeeded.
We recommend using Bun to work with @auldrant/api.
bun installimport { createApi } from '@auldrant/api';
const api = createApi();
const result = await api.get<User[]>('/api/users');
if (result.ok && !result.empty) {
console.log(result.data); // User[] — no null check needed
} else if (!result.ok) {
console.error(result.status); // HTTP status code, or 0 for network errors
}Pass options to createApi to set defaults that apply to every request:
const api = createApi({
baseUrl: 'https://api.example.com',
headers: { Authorization: `Bearer ${token}` },
timeout: 5000,
});
// Relative paths are resolved against baseUrl
const users = await api.get<User[]>('/users');Per-request options always override instance defaults.
All helpers are methods on the instance returned by createApi. Most return Promise<ApiResponse<T>>; .head returns Promise<HeadResponse> (HEAD responses are always empty).
| Method | Signature |
|---|---|
.get |
(url, options?) |
.post |
(url, body?, options?) |
.put |
(url, body?, options?) |
.patch |
(url, body?, options?) |
.delete |
(url, options?) |
.head |
(url, options?) |
.options |
(url, options?) |
Plain objects passed as body are automatically serialized to JSON.
type ApiResponse<T> =
| { ok: true; empty: false; data: T; status: number }
| { ok: true; empty: true; data: null; status: number }
| { ok: false; data: null; status: number };Three variants, discriminated by ok and empty:
| Check | data type |
When |
|---|---|---|
r.ok && !r.empty |
T |
Success with body — the common case, no null check needed |
r.ok && r.empty |
null |
204 No Content, or HEAD response |
!r.ok |
null |
Network failure, timeout, parse error, or non-2xx status |
Status 0 means a network error, timeout, or aborted request. head() returns a narrower HeadResponse type — the non-empty success variant is omitted because HEAD responses never carry a body (RFC 9110 §9.3.5).
| Field | Type | Default | Description |
|---|---|---|---|
accept |
MimeType |
MimeType.JSON |
Expected response MIME type |
headers |
HeadersInit |
— | Additional headers |
signal |
AbortSignal |
— | Cancel the request |
timeout |
number |
— | Abort after N milliseconds |
retry |
number |
0 |
Max additional attempts on network failure |
retryDelay |
number |
0 |
Milliseconds to wait between retries |
Extends RequestOptions with:
| Field | Type | Default | Description |
|---|---|---|---|
contentType |
MimeType |
MimeType.JSON |
Request body content type |
compression |
CompressionMethod |
— | Compress the request body |
| Field | Type | Default | Description |
|---|---|---|---|
baseUrl |
string | URL |
— | Prepended to all relative request paths |
headers |
HeadersInit |
— | Default headers for every request |
accept |
MimeType |
MimeType.JSON |
Default Accept type for every request |
timeout |
number |
— | Default timeout for every request |
// Per-request
const result = await api.get('/data', { timeout: 3000 });
// Or set a default for all requests
const api = createApi({ timeout: 5000 });
const result = await api.get('/data'); // uses 5s timeoutTimed-out requests return { ok: false, status: 0 }.
Retries apply only to network failures (status 0). Server responses (4xx, 5xx) are never retried. The wait between attempts doubles on each retry (exponential backoff).
const result = await api.get('/data', {
retry: 3, // up to 3 additional attempts
retryDelay: 500, // 500ms → 1000ms → 2000ms
});const controller = new AbortController();
const result = await api.get('/slow', { signal: controller.signal });
// Cancel from elsewhere
controller.abort();Aborted requests return { ok: false, status: 0 }.
Headers set in ApiConfig.headers are sent with every request — including absolute URLs that point to a different origin than baseUrl. If you put an Authorization token in config headers, that token travels with any absolute URL you pass to the client.
Best practice: keep Authorization at the call site for requests to trusted origins only, or validate URLs before calling the client. This matches how fetch behaves by default — no library-level stripping.
Compress request bodies before sending (useful for large payloads):
const data = largeJsonPayload;
await api.post('/ingest', data, {
compression: CompressionMethod.GZIP,
});FormData and URLSearchParams bodies are always passed through unchanged — the browser manages their encoding. Small payloads (under 1 KB) are skipped automatically.
| Export | Kind | Description |
|---|---|---|
createApi |
function | Creates a configured API instance |
ApiConfig |
type | Config object for createApi |
ApiInstance |
type | Return type of createApi |
ApiResponse |
type | Discriminated union response type |
HeadResponse |
type | Response shape for HEAD requests (always empty: true on success) |
RequestOptions |
type | Options for GET/DELETE/HEAD/OPTIONS |
RequestBodyOptions |
type | Options for POST/PUT/PATCH |
HttpMethod |
enum | GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS |
HttpStatus |
enum | Common HTTP status codes |
MimeType |
enum | Common MIME type strings |
CompressionMethod |
enum | gzip, deflate |