@auldrant/api
v1.1.0
Published
Simple library for working with APIs
Readme
@auldrant/api
@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 installQuick start
import { 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
}Configured instance
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.
Method helpers
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.
ApiResponse
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).
Options reference
RequestOptions (GET, DELETE, HEAD, OPTIONS)
| 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 |
RequestBodyOptions (POST, PUT, PATCH)
Extends RequestOptions with:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| contentType | MimeType | MimeType.JSON | Request body content type |
| compression | CompressionMethod | — | Compress the request body |
ApiConfig (createApi)
| 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 |
Timeout
// 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 }.
Retry
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
});Abort
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 }.
Credentials and cross-origin requests
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.
Compression
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.
Exports
| 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 |
