name: frontend-api-layer description: Provides patterns for structuring the API layer in React applications. This skill should be used when setting up API clients, defining API request declarations, or integrating with TanStack Query for data fetching.
Frontend API Layer
Patterns for the API layer in React applications using TanStack Query and type-safe API clients.
Canonical Examples
- API Client: client.ts
- Token Store: token-store.ts
- API Declaration: get-task-templates.ts
See references/api-layer-examples.md for full code examples.
Architecture
Component → TanStack Query Hook → API Declaration → API Client → Backend
Core Rules
- API Declarations in
{feature}/api/*.api.tsfiles - Type Safety: Shared types from
@eridu/api-types - Query Keys: Centralize using factory pattern with
listPrefix+list - No FE Data Joins: If a view needs a display field, the primary API must include it. Do not call a second endpoint and join client-side.
- Canonical Resource Routes: Call canonical collection endpoints with
target_type/target_idfilters, not deeply nested paths mirroring component placement. - Decimal Normalization: Normalize both stored API value and user input through the same money helper before comparing.
- AbortSignal: Always destructure
{ signal }fromqueryFncontext and forward to API fetchers.
Query Key Factory Pattern
export const taskTemplateKeys = {
all: ['task-templates'] as const,
lists: () => [...taskTemplateKeys.all, 'list'] as const,
listPrefix: (studioId: string) => [...taskTemplateKeys.lists(), studioId] as const,
list: (studioId: string, filters?: unknown) => [...taskTemplateKeys.listPrefix(studioId), filters] as const,
detail: (id: string) => [...taskTemplateKeys.all, 'detail', id] as const,
};
- Use
listPrefixfor mutation invalidation (clears ALL filter combos for a scope) - Use
list(...)inqueryKeyforuseQuery/useInfiniteQuery - Memoize query key calls in
useMemowhen used outsidequeryKeyoption
Mutation Patterns
- After success: invalidate with
listPrefix, not exactlistkey - Write-through cache:
setQueriesDatafor immediate UI, theninvalidateQueries - Silent mutations (autosave): add
silent?: booleanto variables, guard invalidation withif (!variables.silent), setmeta: { suppressErrorToast: true }
Searchable Lookup Contract
- Build a field-by-field matrix: control name, endpoint, scope, search params, fallback
- Include scope discriminator in query key (
studioId ?? 'admin') for dual-scope helpers - Extract shared
useSearchQueryhelper when 2+ lookup hooks exist in same file - Dead
onSearchwiring = broken implementation, not acceptable placeholder
Route Loader Prefetch
loader: ({ context: { queryClient }, params: { studioId } }) => {
void queryClient.prefetchQuery({ queryKey, queryFn });
},
- Use
void prefetchQuery(non-blocking), notawait ensureQueryData - Match query keys exactly to component
useQuerycalls - Lift queries to parent when child data should be warm before render
Internal Read Freshness Policy
| Tier | Stale time | Example |
|---|---|---|
| Interactive (default) | ~20s | List/detail navigation |
| Operational | ~5s | /me/* task/shift views |
| Lookup/reference | ~1h | Static reference data |
Checklist
- API client configured with Better Auth token management
- All requests in
{feature}/api/*.api.ts - Query keys use factory pattern with
listPrefix - Types from
@eridu/api-types -
signalforwarded to API fetchers - Mutations invalidate via
listPrefix, not exact key - No FE data joins for required display fields
- Money comparisons normalized through same helper
Related Skills
- frontend-state-management — State management patterns
- frontend-error-handling — Error handling
- shared-api-types — Shared API types