| name | project-mtp-data-layer | ||
|---|---|---|---|
| description | mtp-web data layer — fetch through the demo-API seam (apiGet), fixtures served by MSW (visible in Network tab), never hardcode data in data/ or pages; TanStack Query + query-key-factory for server state; one Zustand app store for client/UI state. Pin to ADR-0011. | ||
| metadata |
|
For mtp-web. Authoritative: adr/0011-data-layer-demo-api-query-store.html.
The real mtp-api (.NET) isn't built yet, so screens run on demo APIs served by MSW — apiGet always does a real fetch, and a Mock Service Worker intercepts it in dev and returns dummy JSON (calls show in the DevTools Network tab as real HTTP 200s). Do not hardcode data in pages or in the query hook — fixtures live only in apps/web/src/mocks/.
- Filtering / search / pagination is ALWAYS a POST with a body — never GET query params. Use
apiSearch<T>(path, filters)(POST) for any filtered/searched/paginated read.apiGet<T>(path)takes no params by design and is only for by-key / static resource reads. Endpoint convention:POST /<module>/<resource>/search. The query key must include the filter object so each filter combination caches separately, e.g.useQuery({ queryKey: [...financeKeys.salesRegister.queryKey, filters], queryFn: () => apiSearch<SrData>("/accounting/salesRegister/search", filters) }). - Route names are ALWAYS camelCase — both API endpoints AND frontend route segments. Multi-word segments use camelCase: API
/accounting/salesRegister/search,/finance/bankReconciliation; frontend URLs/finance/bankRecon,/finance/costCentre,/finance/ccReport(thepageIdinnav.tsids +App.tsxpageskeys). Never kebab-case, snake_case, or lowercase-smushed. (Single words / acronyms liketds,gst,ledger,bs,plare already fine.) - Selection controls submit the id, never the label. Combos/dropdowns use
{ value, label }(ComboboxItemadds an optionalhint):value= id/code,label= display name. RHF stores and the API receives thevalue(e.g. account code4001-HDF7710, cost-centreCC-SAL-01) — never the label. Multi-selects submit an array of ids. Server responses for edit forms therefore return ids; resolve the label via the same search/lookup endpoint (ComboboxdisplayValueseeds it).
How to build a new screen's data:
- Types + hook in
apps/web/src/data/<screen>.ts— no data:export function useXxx() { return useQuery({ ...moduleKeys.xxx, queryFn: () => apiGet<XxxData>("/module/xxx") }); }
- Fixture + MSW handler in
apps/web/src/mocks/<screen>.mock.ts— the dummy JSON + an exported handler array; then add its spread tomocks/handlers.ts:import { http, HttpResponse, delay } from "msw"; export const xxxHandlers = [ http.get("/api/module/xxx", async () => { await delay(120); return HttpResponse.json(data); }), ]; // path may carry :params, e.g. /api/finance/accounts/:id/ledger
- Query key in
apps/web/src/keys/<module>.keys.tsviacreateQueryKeys(@lukemorales/query-key-factory). Leaf =xxx: null; dynamic =xxx: (id) => ({ queryKey: [id] }). - Server state → TanStack Query only. Client/UI state → the one Zustand store (
stores/appStore.ts, ui + session slices; persist only ui prefs). Ephemeral screen state staysuseStateor the URL (?tab=). No feature store unless 2+ components share non-server, non-URL state.
Key facts:
apiGet/apiPost/apiPut/apiDelete(lib/api/client.ts) are the only fetch entry points and always do a realfetch(Authorization: Bearer+X-Client-Codeheaders, RFC 7807ProblemDetails→ApiError). In dev (default,VITE_API_MODEunset) MSW intercepts and returns the fixture; calls appear in the DevTools Network tab.main.tsxstarts the worker (mocks/browser.ts) before first render. Flipping to the real API is one env flag (VITE_API_MODE=live— worker never starts) — hooks/pages never change.- The MSW service worker is
public/mockServiceWorker.js(regenerate withnpx msw init publicif it goes missing). - Because the handler awaits, loading/empty/error states are real — handle them (don't assume instant data;
if (!data) return nullflashes briefly). - All 17 built accounting screens now fetch via MSW (
apiGet+financeKeysleaf; fixtures inmocks/*.mock.ts, handlers inmocks/handlers.ts) and show a skeleton onisPending. Migrated 2026-05-23. New screens follow the same pattern from the start. Still pending (separate, smaller scope): converting the few input-bearing screens' hand-rolled inputs to the RHF field kit (ADR-0017) and hand-rolled tables toGridTable— Voucher Entry (static mockup), TDS challan grid, Bank Recon.
Related: [[project-mtp-component-authoring]], [[project-mtp-stack]].