# The typed API client

> How the frontend talks to the API: one typed client, cookies and CSRF handled for you, wrapped in TanStack Query.

## One client, one contract

Every call from the SPA to the API goes through a single typed client. Centralising it means cookies,
CSRF, error handling and base URLs are configured in exactly one place; features just call typed
functions.

```ts
// features/projects/api.ts

```

## Cookies and CSRF

The client sends requests with credentials so the session cookie is included automatically. For
state-changing requests it attaches the CSRF token the API expects. None of this is the feature
author's concern: call `api.post` and the protection is applied.

## Errors

The API returns a consistent error shape, and the client surfaces it as a typed error. Error codes
map to translation keys (snake_case), so the UI can show a localized message without scattering
string literals through components.

```ts
try {
  await createProject({ name })
} catch (err) {
  // err.code -> 'project_name_taken' -> localized message
}
```

## Wrapped in TanStack Query

The raw client functions are wrapped in query and mutation hooks per feature. Components depend on the
hooks, not the transport:

```ts

  const qc = useQueryClient()
  return useMutation({
    mutationFn: createProject,
    onSuccess: () => qc.invalidateQueries({ queryKey: ['projects'] }),
  })
}
```

## Keeping types honest

The request and response types are hand-written to mirror the API's shapes; there is no codegen step
(see [adding a frontend feature](/docs/adding-a-frontend-feature)). Centralising the client keeps every
call site consistent, but keeping those types in line with the API is a manual discipline: rename a
field on the server and the TypeScript still compiles, so the interactive API reference at `/scalar` on
the running API is the source of truth for every endpoint. If you want the wire enforced at build time,
generate the types from the published OpenAPI document (for example with `openapi-typescript`) and
