> [Agent navigation index](/llms.txt)

# Call API contracts

Call API contracts from imperative code or components with shared transport, caching, retry, and typed failure policies.

## Create a reusable API client

Use a client for one origin or policy:

```ts
import { createApiClient } from "brix/contracts";

export const catalogClient = createApiClient({
  baseUrl: "https://catalog.example.com",
  headers: async () => ({ Authorization: `Bearer ${await getAccessToken()}` }),
  timeout: 5_000,
  retry: { maxAttempts: 3, on: [429, 503], respectRetryAfter: true },
  redirect: "error",
});

const result = await catalogClient.execute(getProductApiContract, {
  params: { productId: "123" },
  headers: { "X-Request-ID": requestId },
});

```

`client.execute` returns `ResultPromise<T, ApiRequestError>` and preserves contract inference. Client defaults can include `baseUrl`, headers, timeout, retry, cache, redirect, Fetch `credentials`, response validation, and `onFetch`. Per-call values override defaults; headers merge case-insensitively with call headers winning. Omitted credentials preserve Fetch's `same-origin` default.

An async header resolver runs once per execution before retries, even for a cache hit. Its failure becomes an `ApiRequestError`, not a rejected promise. Timeout covers credential resolution and HTTP work. Use request-scoped server clients for user credentials rather than mutable global current-user state.

Use plain `executeApi(contract, options)` for one-off calls. Both forms are uncached by default and work anywhere with standard Fetch.

## Consume from a component

```tsx
import { useApi } from "brix/hooks";

function ProfileForm() {
  const profileApi = useApi(updateProfileApiContract, {
    onSuccess: (profile) => notifications.saved(profile.id),
  });

  const save = () =>
    profileApi.execute({
      params: { profileId: "profile-123" },
      body: { displayName: "Ada" },
    });

  if (profileApi.isLoading.value) return <Spinner />;
  return (
    <button disabled={profileApi.isFetching.value} onClick={save}>
      Save
    </button>
  );
}

```

`useApi` forwards the same `credentials` option. Exceptions or rejected promises from `onSuccess`
and `onError` are logged without replacing the execution Result or changing hook state.

The hook exposes `data`, `error`, `status`, `isLoading`, `isFetching`, `isSuccess`, `isError`, `execute`, and `reset`. Mounting does not fetch. `isLoading` means a first load with no data; `isFetching` includes background refreshes. Existing data remains visible during refetch.

Every execution returns its own Result. Supersession, reset, and unmount fence stale signal and callback updates. They abort non-cacheable prior work; shared cacheable work continues until its last caller detaches.

Pass a configured client to share transport and cache defaults:

```ts
const productApi = useApi(getProductApiContract, {
  client: catalogClient,
  initialData: serverProduct,
});

```

`initialData` starts local state as successful but does not populate shared storage. Preact applications import from `brix/hooks`; React applications use the matching binding described in [React](/docs/react).

## Opt into native GET caching

```ts
import { createDataCache } from "brix/cache";

export const dataCache = createDataCache();
export const catalogClient = createApiClient({
  baseUrl: "https://catalog.example.com",
  cache: dataCache.public("30s"),
});

const product = await catalogClient.execute(getProductApiContract, {
  params: { productId: "123" },
});

const productApi = useApi(getProductApiContract, { client: catalogClient });
await productApi.execute({ params: { productId: "123" } });

```

The hook can reuse the imperative read because both share one cache. Only validated JSON GETs participate. Mutations, text or streams, disabled response validation, and `cache: false` bypass caching.

Public policy permits sharing. Private reads require an explicit nonsecret partition:

```ts
cache: dataCache.private(session.cacheScope, "30s");

```

Partitioning is not authorization. Rotate the scope on login or permission change and invalidate/reset on logout. Cookie-only identity is not visible to Brix. See [caching](/docs/caching) for TTL, stale fallback, DynamoDB, response vetoes, and security boundaries.

Mutations never invalidate reads automatically:

```ts
await updateProfileApi.execute({ params: { profileId }, body }).unwrap();
await dataCache.invalidate(getProfileApiContract);

```

Awaiting invalidation confirms storage removal. Active hooks refresh in the background; refresh failure updates hook state but does not reject successful invalidation.

## Configure retries deliberately

```ts
const result = executeApi(getProductApiContract, {
  params: { productId: "123" },
  retry: {
    maxAttempts: 3,
    on: [429, 500, 502, 503, 504],
    respectRetryAfter: true,
  },
});

```

`maxAttempts` includes the initial request. Delays use full jitter with a 1,000 ms base and 10,000 ms cap by default. Positive `Retry-After` seconds replace that delay when enabled.

GET, PUT, and DELETE retry network errors and timeouts. POST and PATCH do not unless `retryNonIdempotent: true`; enable that only when an idempotency key or endpoint semantics make duplicate execution safe. Cache stale fallback is considered only after retries finish.

## Handle request failures

```ts
const result = await catalogClient.execute(getProductApiContract, {
  params: { productId: "123" },
});

if (result.isErr()) {
  result.error.kind;
  result.error.status;
  result.error.data;
  result.error.hasStatus(404);
}

```

Error kinds are `http`, `network`, `timeout`, `aborted`, `decode`, and `response-validation`. Treat them as expected typed outcomes; use `.unwrap()` only where converting an error into a panic is intentional. See [results](/docs/results).

Cross-origin browser clients need an explicit base URL and a matching server CORS allowlist. Set `redirect: "error"` for server calls where redirects could cross a trust boundary. Test handlers through the route harness and external calls with contract mocks; see [testing](/docs/testing).
