API Client
createApiClient is the central API layer. It handles base URLs, dynamic headers, auth tokens, request cancellation, timeouts, retries, normalized errors, and an auth refresh queue.
Create a Client
import { createApiClient } from "statemesh-core";
const api = createApiClient({
baseUrl: "/api",
getAccessToken: () => authStore.token,
refreshAuth: () => authStore.refresh(),
timeout: 10_000,
retry: {
attempts: 3,
delay: ({ attempt }) => attempt * 500,
retryOn: [408, 429, 500, 502, 503, 504],
retryNetworkErrors: true,
retryTimeouts: false,
jitter: true
},
onEvent: (event) => {
console.debug("[api]", event.type, event);
}
});Options
| Option | Type | Default | Description |
|---|---|---|---|
baseUrl | string | — | Base URL for all requests. Relative paths work naturally. |
headers | Record<string, string> | — | Default headers for all requests |
getAccessToken | () => string | null | — | Dynamic auth token getter |
refreshAuth | () => Promise<void> | — | Auth refresh function. Concurrent 401s share one refresh call. |
timeout | number | false | 10000 | Request timeout in ms. false disables. |
retry | RetryOptions | — | Global retry configuration |
onEvent | (event) => void | — | Event hook for logging, analytics |
HTTP Methods
// GET
const products = await api.get<Product[]>("/products", {
query: { search: "keyboard", page: 1 }
});
// POST
const product = await api.post<Product>("/products", {
body: { name: "Keyboard", price: 99 }
});
// PUT
const updated = await api.put<Product>("/products/1", {
body: { name: "Mechanical Keyboard" }
});
// PATCH
const patched = await api.patch<Product>("/products/1", {
body: { price: 79 }
});
// DELETE
await api.delete("/products/1");Per-Request Overrides
Every control can be overridden per request:
api.get("/products", {
timeout: false,
retry: {
attempts: 1,
delay: 250,
retryOn: (ctx) => ctx.status === 503
},
signal: abortController.signal,
headers: { "X-Custom": "value" }
});Upload
Send FormData, File, Blob, or other browser bodies:
const formData = new FormData();
formData.append("avatar", file);
await api.upload<User>("/profile/avatar", formData, {
onUploadProgress(progress) {
console.log(`${progress.percent}% uploaded`);
}
});Auth Refresh Queue
When a 401 is received, the client calls refreshAuth() once and queues all concurrent requests. After the refresh completes, queued requests retry automatically:
const api = createApiClient({
baseUrl: "/api",
getAccessToken: () => token,
refreshAuth: async () => {
const result = await fetch("/api/auth/refresh");
token = result.accessToken;
}
});
// Multiple concurrent 401s share one refresh call
const [user, products] = await Promise.all([
api.get("/me"),
api.get("/products")
]);Relative Base URLs
const api = createApiClient({ baseUrl: "/api" });
api.get("/products"); // Sends to /api/productsError Handling
All errors are normalized to ApiClientError:
import { ApiClientError, isApiClientError } from "statemesh-core";
try {
await api.get("/protected");
} catch (error) {
if (isApiClientError(error)) {
console.log(error.status); // 401
console.log(error.message); // "Unauthorized"
console.log(error.headers); // Response headers
}
}Events
The onEvent callback receives events for request lifecycle:
onEvent: (event) => {
switch (event.type) {
case "api.request": // Request started
case "api.response": // Response received
case "api.error": // Request failed
case "api.retry": // Retry attempted
case "api.timeout": // Request timed out
case "api.auth_refresh": // Auth token refreshed
}
}Important Notes
TIP
Relative API bases work naturally in frontend apps. baseUrl: "/api" with api.get("/products") sends /api/products.
WARNING
Always pass an AbortSignal when the request can be cancelled (e.g., in a resource's fetch function or a transaction's effect). This prevents wasted network requests.
TIP
The retry configuration uses the same backoff() helper available for transactions: delay: backoff({ base: 500, max: 10000, jitter: true }).
Next Steps
- Resources — Use the API client with cached resources
- Mutations — Use the API client for writes
- Persistence — Persist state and cache
