---
name: frontend-api-client
description: >
  Generates TypeScript service clients + React hooks (useState/useEffect) using
  the `api` HTTP client from @atlashub/smartstack (pre-configured, returns
  unwrapped data) and direct REST paths (/api/{module}/{plural}).
phase: development/frontend
cli: cli/scaffold-api-client
allowed-tools: [Read, Glob, Grep, Bash]  # Bash: CLI invocation
---

# API Client — TypeScript Services + React Hooks

Generates API service clients and React hooks (useState/useEffect) for entity CRUD operations.

Direct REST paths are used (`/api/{module}/{plural-kebab}`), not a client-side
route registry: `navRoutes.generated.ts` was removed from the SDK, so generated
code talks to the API via direct paths.

## Output pattern

### Types (`types/index.ts`)

```ts
export interface EmployeeListDto {
  id: string;
  firstName: string;
  lastName: string;
  createdAt: string;
}

export interface EmployeeDetailDto { ... }
export interface CreateEmployeeDto { firstName: string; lastName: string }
export interface UpdateEmployeeDto { firstName?: string; lastName?: string }
export interface PaginatedResult<T> { items: T[]; totalCount: number; page: number; pageSize: number }
```

### Service (`services/employeeService.ts`)

```ts
import { api } from '@atlashub/smartstack';
import type { EmployeeListDto, EmployeeDetailDto, CreateEmployeeDto, UpdateEmployeeDto, PaginatedResult } from '../types';

const API_PATH = '/api/hrm/employees';

export const employeeService = {
  getAll: async (params?: { page?: number; pageSize?: number; search?: string }) => {
    const data = await api.get<PaginatedResult<EmployeeListDto>>(API_PATH, { params });
    return data;
  },
  getById: async (id: string) => {
    const data = await api.get<EmployeeDetailDto>(`${API_PATH}/${id}`);
    return data;
  },
  create: async (payload: CreateEmployeeDto) => {
    const data = await api.post<string>(API_PATH, payload);
    return data;
  },
  update: async (id: string, payload: UpdateEmployeeDto) => {
    await api.put(`${API_PATH}/${id}`, payload);
  },
  delete: async (id: string) => {
    await api.delete(`${API_PATH}/${id}`);
  },
};
```

`api` is the pre-configured HTTP client from `@atlashub/smartstack` — it returns
unwrapped data (not `AxiosResponse`), preconfigured with:
- `baseURL` from `SmartStackProvider` config
- Auth cookie (httpOnly, `withCredentials: true`)
- Tenant header from the active tenant selection
- Retry on 5xx
- Error logging

### Hooks (`hooks/useEmployee.ts`)

```ts
import { useState, useEffect, useCallback } from 'react';
import { employeeService } from '../services/employeeService';
import type { CreateEmployeeDto, UpdateEmployeeDto } from '../types';

export function useEmployees(params?) {
  const [data, setData] = useState<...>();
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const refetch = useCallback(() => {
    setIsLoading(true);
    employeeService.getAll(params).then(setData).catch(setError).finally(() => setIsLoading(false));
  }, [JSON.stringify(params)]);
  useEffect(() => { refetch(); }, [refetch]);
  return { data, isLoading, error, refetch };
}

export function useEmployee(id: string) { ... }
export function useCreateEmployee() { ... }  // returns { mutateAsync, isPending }
export function useUpdateEmployee() { ... }
export function useDeleteEmployee() { ... }
```

## ⚠ BLOCKING — Key Rules

1. **Import `api`** from `@atlashub/smartstack` — not a local `apiClient` or `axios` instance. `api` returns unwrapped data (no `{ data }` destructuring needed).
2. **Direct REST paths — strata-aware (Wave E)**. The generator accepts a
   `useScreens` boolean flag that switches between the two API strata:

   | `useScreens` | API_PATH | get/post routes | delete + getLookup |
   |--------------|----------|-----------------|--------------------|
   | `false` (default) | `/api/{module}/{plural-kebab}` | REST CRUD (GET /, GET /{id}, POST /, PUT /{id}) | same prefix |
   | `true` | `/api/screens/{plural-kebab}` | screen-driven (GET /list, GET /detail/{id}, POST /form, PUT /form/{id}) | falls back to `/api/v1/integration/{plural-kebab}` (declared in `INTEGRATION_PATH`) |

   The screen-driven contract intentionally omits `delete` + `getLookup` — both
   continue to be served by the integration stratum (same Business layer, just
   a different transport). Set `useScreens: true` once Phase 2b of
   `/ba-develop` has produced the corresponding `{EntityPlural}ScreenController.cs`.
   Audit `DEV-API-012` (Wave D) verifies coverage on the backend side.

   **Wave F1 — `${E}ListDto` / `${E}DetailDto` shape**. In screen-driven mode the
   TS DTOs are shaped from the **pagespec columns**, NOT from `entity.fields`.
   Pass `screenColumns: { list, detail }` per entity alongside `useScreens: true`:

   ```ts
   {
     name: 'Contact',
     fields: [...],            // domain shape — still used by Create/Update Dtos
     screenColumns: {
       list:   [ { key: 'fullName', formatHint: 'string' }, { key: 'status', formatHint: 'string' } ],
       detail: [ { key: 'fullName', formatHint: 'string' }, { key: 'totalOrders', formatHint: 'integer' } ],
     },
   }
   ```

   The TS type of each column mirrors `scaffold-screen-controller`'s
   `dotnetTypeFor` so the wire matches byte-for-byte (`currency/integer →
   number`, `datetime → string`, `boolean → boolean`, fallback `string`). When
   `screenColumns` is omitted the generator falls back to the legacy heuristic
   (first 5 `entity.fields`) — useful for projects mid-migration. When
   `useScreens=false`, `screenColumns` is ignored (legacy mode untouched).
3. **Unknown field types throw** — the CLI fails fast on unsupported types rather than emitting `unknown` silently.
4. **Guard `if (!id) return;`** in detail hooks — avoids a 404 call when id is empty.
5. **`refetch` returned** from list hooks — pages can re-fetch after mutations.
6. **`PaginatedResult<T>`** exported alongside DTOs so pages can type their table data.

## Spec fields

| Field | Type | Required | Notes |
|---|---|---|---|
| `module` | string | yes | Module code (kebab-case). |
| `appCode` | string | yes | App code (kebab-case). |
| `entities` | array | yes | Entity list with `name`, `section`, `fields`, `customActions`, `screenColumns`. |
| `projectPath` | string | yes | Absolute path to the project root. |
| `routeMode` | enum | no | Route convention: `direct` (default), `integration`, `screens`. Backward compat: `useScreens: true` = `'screens'`. |
| `useScreens` | boolean | no | Deprecated alias for `routeMode: 'screens'`. Default `false`. |
| `httpClient` | enum | no | HTTP client source: `smartstack` (default, uses `api` from `@atlashub/smartstack`) or `axios` (local instance). |
| `webRoot` | string | no | Relative path from `projectPath` to web root. Default `web/{appCode}-web`. |

## Invocation

```bash
npx --prefer-offline tsx skills/development/frontend/api-client/cli/scaffold-api-client/index.ts \
  --spec '{"module":"hrm","appCode":"myapp","entities":[{"name":"Employee","section":"employees","fields":[{"name":"firstName","type":"string","required":true}]}],"projectPath":"/path"}'
```

## Output files (per entity)

```
src/features/{module}/{entity}/
├── types/index.ts
├── services/{entity}Service.ts
└── hooks/use{Entity}.ts
```
