# Step 10 — The API Client: `tagsApi` in `lib/api.ts`

Step 8's Axum routes accept and return JSON over HTTP; Step 9's `shared/types.ts` describes that JSON's shape in TypeScript. `vibe-frontend/src/lib/api.ts` is the layer that actually makes the HTTP calls and hands back typed data — every feature in iKanban has a small object like `tagsApi` here.

## `tagsApi` — one function per backend route

```ts
export const tagsApi = {
  list: async (params?: TagSearchParams): Promise<Tag[]> => {
    const queryParams = new URLSearchParams();
    if (params?.team_id) queryParams.set('team_id', params.team_id);
    const response = await makeRequest(`/api/tags?${queryParams.toString()}`);
    return handleApiResponse<Tag[]>(response);
  },

  create: async (data: CreateTag): Promise<Tag> => {
    const response = await makeRequest('/api/tags', {
      method: 'POST',
      body: JSON.stringify(data),
    });
    return handleApiResponse<Tag>(response);
  },

  update: async (tagId: string, data: UpdateTag): Promise<Tag> => {
    const response = await makeRequest(`/api/tags/${tagId}`, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
    return handleApiResponse<Tag>(response);
  },

  delete: async (tagId: string): Promise<void> => {
    const response = await makeRequest(`/api/tags/${tagId}`, { method: 'DELETE' });
    return handleApiResponse<void>(response);
  },
};
```

Notice the one-to-one match with Step 8's router: `GET /tags`, `POST /tags`, `PUT /tags/{tag_id}`, `DELETE /tags/{tag_id}` on the Rust side become `tagsApi.list`, `.create`, `.update`, `.delete` here — same four operations, same argument shapes (`CreateTag` in, `Tag` out for `create`; `UpdateTag` + an id in, `Tag` out for `update`), imported straight from Step 9's `shared/types`.

## `makeRequest` — the shared fetch wrapper

Every `tagsApi` method calls `makeRequest`, not `fetch` directly. `makeRequest` (defined once, higher up in the same file) is where cross-cutting concerns live so individual API objects like `tagsApi` don't have to repeat them:

- Attaches the `Authorization: Bearer <token>` header automatically, reading the current auth token.
- Sets `Content-Type: application/json` by default.
- Runs requests through a **circuit breaker** — if the backend is failing broadly, `makeRequest` fails fast instead of piling up retries against a service that's already down.

## `handleApiResponse` — unwrapping the backend's envelope

Recall Step 8's handlers return `Ok(ApiResponse::success(tags))`. On the frontend, `handleApiResponse<T>(response)`:
1. If `!response.ok`, reads the backend's error body — checking both `{ message }` and `{ error }` shapes, since some older routes and the canonical `ErrorResponse` (Step 8) differ — and throws a typed `ApiError`.
2. Otherwise parses the JSON body as `ApiResponse<T>` and, if `result.success` is `true`, returns just `result.data` — **unwrapped**. This is why `tagsApi.list()`'s return type is `Promise<Tag[]>`, not `Promise<ApiResponse<Tag[]>>`: every caller works with the plain data, never the envelope.

This is the pattern referenced by CLAUDE.md's guidance to "verify API shape at the call-site" — any new frontend code consuming an endpoint should check a sibling `xApi` object in this same file rather than guess whether a response needs unwrapping.

## Checkpoint

Find `TagSearchParams` in `shared/types.ts` (used by `tagsApi.list`'s parameter) and compare its optional `search`/`team_id` fields to `ListTagsQuery` from Step 8's Rust route — same two query-string parameters, described independently on each side of the network boundary, exactly like `Tag`/`Tag` (Rust) from Step 9.

Continue to **Step 11 — The React Hook: `useTags` with TanStack Query**, where `tagsApi` gets wired into a component-friendly hook.
