# Data Fetching

This document covers the full data-fetching stack: API client creation, authentication injection, query state management, and URL synchronization.

---

## Layer Overview

```
useAuthApi()
  └─ createApi({ headers: { Authorization: "Bearer <token>" } })
       └─ new CustomersClient(...)   ← one per domain (CrudClient subclass)
            └─ ApiClient             ← openapi-fetch wrapper, type-safe from OpenAPI schema
```

```
useDataTableQuery({ queryKey, client, queryOptions })
  └─ React Query useQuery
       └─ client.list({ page, per_page, sort_by, sort_order })
  └─ nuqs useQueryStates          ← URL ↔ pagination & sort params
```

---

## `useAuthApi` — Authenticated API Factory

**File:** `shared/useApi.ts`

```ts
import { useAuthApi } from "@/shared/useApi"

const api = useAuthApi()
// api.customers, api.vehicles, api.employees, …
```

Reads the JWT token from the `useAuthStore` Zustand store and passes it as the `Authorization: Bearer <token>` header. Called inside any component or hook that needs to make authenticated requests.

> **Note:** `createApi()` is called on every render. If performance is a concern, wrap in `useMemo` (see [Enhancement Plan](./enhancement-plan.md)).

### Server-Side Variant

For `async` server components or server actions:

```ts
import { getAuthApi } from "@/shared/api"

const api = await getAuthApi()  // reads token from cookies (Next.js server-side)
```

---

## `CrudClient` — Generic CRUD Base Class

**File:** `packages/api/src/infra/crud-client.ts`

All domain clients extend `CrudClient`. It provides four standard operations:

| Method | HTTP | Endpoint |
|---|---|---|
| `list(query?)` | `GET` | `indexRoute` (e.g. `/api/customers`) |
| `create(payload)` | `POST` | `indexRoute` |
| `update(id, payload)` | `PUT` | `byIdRoute` (e.g. `/api/customers/{id}`) |
| `destroy(id)` | `DELETE` | `byIdRoute` |

All methods are **fully type-safe** — parameter types, request body shapes, and response types are all derived from the OpenAPI schema via `packages/api/types/index.ts`.

### Exported Type Utilities

```ts
// Extract the list response type from a client class
type CrudListResponse<C>  // e.g. { data: Customer[], meta: { last_page, total, ... } }

// Extract a single item type from the list data array
type CrudListItem<C>      // e.g. Customer

// Extract query params accepted by list()
type CrudListParams<C>

// Base interface: all list items have `id: number`
type BaseCrudItem = { id: number }
```

### Example: Creating a Domain Client

```ts
// packages/api/src/clients/my-resource.ts
import { CrudClient } from "../infra/crud-client"

export const MY_ROUTES = {
  INDEX: "/api/my-resources",
  BY_ID: "/api/my-resources/{id}",
} as const

export class MyResourceClient extends CrudClient<
  typeof MY_ROUTES.INDEX,
  typeof MY_ROUTES.BY_ID
> {
  constructor(baseUrl?: string, options?: ApiClientOptions) {
    super(baseUrl, options, MY_ROUTES.INDEX, MY_ROUTES.BY_ID)
  }

  // Add domain-specific endpoints here:
  async listCategories() {
    return this.get("/api/my-resource-categories")
  }
}
```

Then register it in `packages/api/src/api.ts`:

```ts
export function createApi(options?: ApiClientOptions) {
  return {
    // ...
    myResources: new MyResourceClient(undefined, options),
  }
}
```

---

## `ApiClient` — Low-Level HTTP Client

**File:** `packages/api/src/infra/client.ts`

Wraps `openapi-fetch`. All requests are typed against `paths` from `packages/api/types/index.ts`, which is generated from the OpenAPI schema.

### Error Handling

Failed requests throw an `ApiError`:

```ts
class ApiError extends Error {
  status: number           // HTTP status code
  statusText: string
  endpoint: string
  method: string
  payload?: {
    message?: string
    errors?: Record<string, string[]>  // Laravel validation errors
  }

  get validationErrors(): Record<string, string[]> | undefined
}
```

### `ApiError` in Form Context

In mutation `onError` handlers, check for validation errors and apply them to individual form fields:

```ts
onError: (err) => {
  if (err instanceof ApiError && err.validationErrors) {
    Object.entries(err.validationErrors).forEach(([field, messages]) => {
      form.setError(field as any, { message: messages[0] })
    })
  }
}
```

---

## `useDataTableQuery` — Paginated List + URL State

**File:** `shared/data-view/table-view/use-data-table-query.ts`

Wraps React Query + `nuqs` to keep the table's pagination and sort state synchronized with the URL.

```ts
const tableQuery = useDataTableQuery({
  queryKey: ["customers"],      // React Query cache key prefix
  client,                       // Any object with a .list(query?) method
  queryOptions,                 // Optional React Query overrides (staleTime, etc.)
})
```

### Returns

| Key | Description |
|---|---|
| `data` | The raw API response (`CrudListResponse<C>`) |
| `isLoading` | True during initial fetch |
| `pagination` | `{ page, pageSize, pageCount: 1, total: 0 }` — pageCount/total come from `data.meta` |
| `sorting` | `SortingState` derived from URL params |
| `params` | Raw parsed URL params (`page`, `per_page`, `sort_by`, `sort_order`) |
| `setParams` | Direct URL param setter |
| `handleChange` | Normalized event handler for `DataTable` (see below) |
| `invalidateQuery` | Busts the cache (called after mutations) |

### URL Query Parameters

| Param | Default | Description |
|---|---|---|
| `page` | `1` | Current page (1-based) |
| `per_page` | `10` | Rows per page |
| `sort_by` | `null` | Column `accessorKey` to sort by |
| `sort_order` | `null` | `"asc"` or `"desc"` |

### `handleChange` Event Types

```ts
// Triggered by DataViewPagination (page navigation, rows per page)
{ type: "pagination", pagination: { page, pageSize, ... } }

// Triggered by ColumnHeader sort dropdown
{ type: "sorting", sorting: [{ id: "email", desc: false }] }
// → resets page to 1 automatically
```

---

## `DataTable` — Table UI

**File:** `shared/data-view/table-view/data-table.tsx`

Thin wrapper around TanStack Table v8 with manual server-side pagination and sorting:

```tsx
<DataTable
  columns={columns}
  data={data}
  pagination={{ page, pageSize, pageCount, total }}
  sorting={sorting}
  onChange={handleChange}
  isLoading={isLoading}
/>
```

While `isLoading` is `true`, the table renders `pageSize` skeleton rows instead of data.

### `ColumnHeader` — Sortable Column Header

```tsx
import { ColumnHeader } from "@/shared/data-view/table-view"

{
  accessorKey: "email",
  header: ({ column }) => <ColumnHeader column={column} title="Email" />,
}
```

Renders a sort dropdown (Asc / Desc / Clear) if the column `canSort`. Shows a plain `<span>` otherwise.

---

## Auth Store

**File:** `shared/stores/auth-store.ts`

A Zustand store that holds the authenticated user state:

| Key | Type | Description |
|---|---|---|
| `token` | `string \| undefined` | JWT access token |
| `user` | `AuthUser \| undefined` | Authenticated user profile |
| `isAuthenticated` | `boolean` | True when token + user are set |
| `login(token, user, expiresIn?)` | fn | Persists to cookie + sets store |
| `logout()` | fn | Calls API logout, clears cookie + store |
| `hydrate()` | fn | Reads cookies on app boot (call in root layout) |

---

## Type System — OpenAPI-Derived Types

The entire API type surface is generated from `packages/api/open-api/schema.yaml` via scripts in `packages/api/scripts/`. The generated output is `packages/api/types/index.ts`.

Key exported type helpers from `packages/api/src/infra/types.ts`:

| Type | Description |
|---|---|
| `ApiPath` | Union of all known API paths |
| `ApiPathByMethod<M>` | All paths that support HTTP method `M` |
| `ApiQueryParams<Path, Method>` | Query parameter shape for a given path+method |
| `ApiRequestBody<Path, Method>` | Request body shape |
| `ApiResponse<Path, Method>` | Successful response shape |
| `ApiPathParams<Path, Method>` | URL path parameters (e.g. `{ id: string }`) |

These types flow through `CrudClient` → `useDataTableQuery` → `ResourcePage` → feature page, providing end-to-end type safety without any manual typing.
