feat(api-client): generated typed client + TanStack Query hooks (#182) #197

Merged
james merged 2 commits from 182-generated-api-client into main 2026-06-21 04:26:16 +00:00
Owner

Summary

Fills in packages/api-client (the placeholder from #181) with a typed HTTP client + TanStack Query hooks the universal Expo client (#183) will consume. ADR-0027 §5 mandated the shape: "The hook layer is hand-written and thin; the OpenAPI tooling generates typed fetch calls, and the hooks wire them into TanStack Query." This PR is that shape, literally.

Two commits:

  • d147811 — package + source (3179-line types generated from openapi.json; client wrapper with auth-injection + Problem Details normalization; ~30 hooks across 13 resources; tests covering the client / error / keys layers; RN-gated setup under ./native).
  • 723c47b — CI gate additions (@carol/api-client lint/typecheck/test/check joined to the existing jobs), docs/api-conventions.md §"Consumer-side codegen", CLAUDE.md TanStack bullet extension.

Decisions baked in

  • openapi-typescript + openapi-fetch + hand-written hooks. Rejected orval / kubb (own hooks layer; contradicts ADR-0027) and openapi-zod-client (duplicates server contract enforcement at runtime).
  • Generated files committed, mirroring openapi.json. CI gate (pnpm -F @carol/api-client check) hard-fails on drift.
  • Auth via callback: createApiClient({ baseUrl, getAuthHeader? }). Web omits the callback (cookies flow via credentials: "include"); native passes an async reader from SecureStore.
  • Errors normalize to CarolApiError with the RFC 7807 fields. TanStack Query's error is a typed object, not a raw fetch failure.
  • One hook per (resource, operation). ~30 hooks: list / detail / create / update / delete patterns + useNotesInfinite for the one paginated endpoint + useMove* for skills.
  • Platform gating via subpath: @carol/api-client/native exports setupNativeQuery() wiring TanStack's onlineManager / focusManager to NetInfo + AppState. Inline-typed dynamic imports — no RN deps in the core.
  • No PWA migration in this PR. apps/api/app/(app)/notes/notes-client.tsx and friends keep fetch(...) directly until #185 deletes them.

Out of scope

  • Filling in apps/client/ (#183).
  • Adding RN deps as installed packages (peer-only; Expo brings them).
  • Hook coverage for sort/filter (#192) or pagination on flat lists (#191) — hooks anticipate the shapes but don't drive the migrations.
  • Migrating PWA screens to use the new hooks.

Test plan

  • pnpm install clean.
  • pnpm -F @carol/api-client typecheck green.
  • pnpm -F @carol/api-client lint green.
  • pnpm -F @carol/api-client test green (16 tests across client / error / keys).
  • pnpm -F @carol/api-client check exits 0.
  • pnpm -F @carol/api openapi:check and openapi:coverage still green (54 routes).
  • Reproduced the full pipeline in a Node 22.23.0 container matching CI — all green.
  • lefthook clean on each commit.

Closes #182. Fourth ticket under epic #176. Unblocks #183 (Expo client scaffolding).

## Summary Fills in `packages/api-client` (the placeholder from #181) with a typed HTTP client + TanStack Query hooks the universal Expo client (#183) will consume. ADR-0027 §5 mandated the shape: *"The hook layer is hand-written and thin; the OpenAPI tooling generates typed fetch calls, and the hooks wire them into TanStack Query."* This PR is that shape, literally. Two commits: - **`d147811`** — package + source (3179-line types generated from `openapi.json`; client wrapper with auth-injection + Problem Details normalization; ~30 hooks across 13 resources; tests covering the client / error / keys layers; RN-gated setup under `./native`). - **`723c47b`** — CI gate additions (`@carol/api-client` lint/typecheck/test/check joined to the existing jobs), `docs/api-conventions.md` §"Consumer-side codegen", `CLAUDE.md` TanStack bullet extension. ## Decisions baked in - **`openapi-typescript` + `openapi-fetch` + hand-written hooks.** Rejected `orval` / `kubb` (own hooks layer; contradicts ADR-0027) and `openapi-zod-client` (duplicates server contract enforcement at runtime). - **Generated files committed**, mirroring `openapi.json`. CI gate (`pnpm -F @carol/api-client check`) hard-fails on drift. - **Auth via callback**: `createApiClient({ baseUrl, getAuthHeader? })`. Web omits the callback (cookies flow via `credentials: "include"`); native passes an async reader from `SecureStore`. - **Errors normalize to `CarolApiError`** with the RFC 7807 fields. TanStack Query's `error` is a typed object, not a raw fetch failure. - **One hook per (resource, operation).** ~30 hooks: list / detail / create / update / delete patterns + `useNotesInfinite` for the one paginated endpoint + `useMove*` for skills. - **Platform gating via subpath**: `@carol/api-client/native` exports `setupNativeQuery()` wiring TanStack's `onlineManager` / `focusManager` to NetInfo + AppState. Inline-typed dynamic imports — no RN deps in the core. - **No PWA migration in this PR.** `apps/api/app/(app)/notes/notes-client.tsx` and friends keep `fetch(...)` directly until #185 deletes them. ## Out of scope - Filling in `apps/client/` (#183). - Adding RN deps as installed packages (peer-only; Expo brings them). - Hook coverage for sort/filter (#192) or pagination on flat lists (#191) — hooks anticipate the shapes but don't drive the migrations. - Migrating PWA screens to use the new hooks. ## Test plan - [x] `pnpm install` clean. - [x] `pnpm -F @carol/api-client typecheck` green. - [x] `pnpm -F @carol/api-client lint` green. - [x] `pnpm -F @carol/api-client test` green (16 tests across client / error / keys). - [x] `pnpm -F @carol/api-client check` exits 0. - [x] `pnpm -F @carol/api openapi:check` and `openapi:coverage` still green (54 routes). - [x] Reproduced the full pipeline in a Node 22.23.0 container matching CI — all green. - [x] lefthook clean on each commit. Closes #182. Fourth ticket under epic #176. Unblocks #183 (Expo client scaffolding).
Fills in the workspace placeholder from #181. Package boundary only —
no source code yet (subsequent commits land that).

- type: module, exports `.` and `./native` (RN-only subpath so web
  bundles don't pull react-native).
- dependencies: @tanstack/react-query (matching apps/api's ^5.101.0)
  + openapi-fetch as the typed runtime.
- devDependencies: openapi-typescript (codegen), tsx (script runner),
  vitest, typescript, eslint, typescript-eslint, @types/node (the
  scripts use node:fs / node:path), @types/react, react.
- peerDependencies: react ^19; react-native + @react-native-community/netinfo
  as OPTIONAL peers — only consumers using the ./native subpath need
  them.
- tsconfig.json: strict mode, moduleResolution: bundler, noEmit.
- vitest.config.ts: environment node, tests under tests/.
- eslint.config.mjs: typescript-eslint strict; no eslint-config-next
  (no Next.js dep here). Test files exempt from no-non-null-assertion
  to match apps/api's convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docs(api-client): wire @carol/api-client into CI + conventions doc + CLAUDE.md (#182)
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 7s
PR / Static analysis (pull_request) Successful in 30s
PR / OSV-Scanner (pull_request) Failing after 23s
PR / Package age policy (soft) (pull_request) Successful in 15s
Secrets / gitleaks (pull_request) Successful in 12s
PR / Typecheck (pull_request) Successful in 3m38s
PR / Lint (pull_request) Successful in 3m34s
PR / OpenAPI (pull_request) Successful in 3m49s
PR / Test (sqlite) (pull_request) Successful in 2m39s
PR / pnpm audit (pull_request) Successful in 2m41s
PR / Build (pull_request) Successful in 4m17s
PR / Coverage (soft) (pull_request) Successful in 3m1s
PR / Test (postgres) (pull_request) Failing after 4m24s
PR / Trivy (image) (pull_request) Failing after 3m56s
723c47ba26
Round out the package work with the cross-cutting touches:

- .forgejo/workflows/pr.yml — extend the lint, typecheck, openapi,
  and test jobs to also run the @carol/api-client equivalents. The
  package's check (drift gate) joins the openapi job; its tests run
  once per matrix leg (only on the sqlite leg — they have no DB).
- docs/api-conventions.md §"Spec generation" — append a
  "Consumer-side codegen" subsection naming openapi-typescript +
  openapi-fetch + hand-written hooks, and the rejected alternatives
  (orval, kubb, openapi-zod-client) with one-line rationales.
- CLAUDE.md "Stack defaults" — extend the TanStack bullet with a
  one-liner pointing at packages/api-client/README.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

OSV-Scanner

Threshold: high  ·  Total findings: 7  ·  At/above threshold: 2

critical high medium low
1 1 5 0
severity id package installed / range fix
critical GHSA-5xrq-8626-4rwp vitest@2.1.9 4.0.0–4.1.0 4.1.0
high GHSA-fx2h-pf6j-xcff vite@5.4.21 8.0.0–8.0.16 8.0.16
<!-- scanner-comment: osv --> ### OSV-Scanner **Threshold:** `high` &nbsp;·&nbsp; **Total findings:** 7 &nbsp;·&nbsp; **At/above threshold:** 2 | critical | high | medium | low | |---:|---:|---:|---:| | 1 | 1 | 5 | 0 | | severity | id | package | installed / range | fix | |---|---|---|---|---| | critical | GHSA-5xrq-8626-4rwp | vitest@2.1.9 | 4.0.0–4.1.0 | `4.1.0` | | high | GHSA-fx2h-pf6j-xcff | vite@5.4.21 | 8.0.0–8.0.16 | `8.0.16` |

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 61.8% ≥ 50%
Branches 80.3% ≥ 75%
Functions 87.9% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 61.8% ✅ | ≥ 50% | | Branches | 80.3% ✅ | ≥ 75% | | Functions | 87.9% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.

Trivy (container image)

Threshold: high  ·  Total findings: 121  ·  At/above threshold: 1

critical high medium low
0 1 50 70
severity id package installed / range fix
high CVE-2026-12151 undici 6.25.0 6.27.0, 7.28.0, 8.5.0
<!-- scanner-comment: trivy --> ### Trivy (container image) **Threshold:** `high` &nbsp;·&nbsp; **Total findings:** 121 &nbsp;·&nbsp; **At/above threshold:** 1 | critical | high | medium | low | |---:|---:|---:|---:| | 0 | 1 | 50 | 70 | | severity | id | package | installed / range | fix | |---|---|---|---|---| | high | [CVE-2026-12151](https://avd.aquasec.com/nvd/cve-2026-12151) | undici | 6.25.0 | `6.27.0, 7.28.0, 8.5.0` |
james merged commit ecf7f8c51f into main 2026-06-21 04:26:16 +00:00
james deleted branch 182-generated-api-client 2026-06-21 04:26:16 +00:00
Sign in to join this conversation.
No description provided.