# 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 " } }) └─ 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 ` 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 // e.g. { data: Customer[], meta: { last_page, total, ... } } // Extract a single item type from the list data array type CrudListItem // e.g. Customer // Extract query params accepted by list() type CrudListParams // 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 // Laravel validation errors } get validationErrors(): Record | 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`) | | `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 ``` 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 }) => , } ``` Renders a sort dropdown (Asc / Desc / Clear) if the column `canSort`. Shows a plain `` 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` | All paths that support HTTP method `M` | | `ApiQueryParams` | Query parameter shape for a given path+method | | `ApiRequestBody` | Request body shape | | `ApiResponse` | Successful response shape | | `ApiPathParams` | 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.