ADR-0013: Centralized API layer & server-state discipline

  • Status: Accepted
  • Date: 2026-06-05
  • Scope: core (any frontend consuming a REST API)

Context

API access decays into chaos through three habits: URLs hardcoded at call sites
(unsearchable, drift-prone), React Query cache keys written as ad-hoc arrays
(invalidation misses them), and auth/error handling re-implemented per request.
Each is invisible at review time and expensive at debug time.

Decision

Three single sources of truth per app:

  • src/api/endpoints.ts — every endpoint as an ENDPOINTS const
    (path-only builders for parameterized routes). Never hardcode a URL at a
    call site.
  • src/api/queryKeys.ts — a query-key factory
    (queryKeys.<domain>.list(params) / .detail(id) / .all). Never write a
    raw key array.
  • src/api/axios.ts — one client instance owning cross-cutting concerns
    via interceptors (bearer token injection; 401 → clear session + redirect;
    app-specific response handling). Request code never deals with auth.

Server-state conventions (React Query):

  • Mutations use mutateAsync when the caller needs to await or catch;
    on success, invalidate the relevant query keys (via the factory) so the
    cache never serves stale entities. Direct setQueryData is the exception for
    instant-feedback updates, not the rule.
  • Shared types mirror backend DTO names exactly (see the backend’s
    0011-dto-design-rules) — a DTO name is a cross-repo search key. Top-level
    types stay complete; slim embedded types follow <Parent><Child>Response.

Consequences

  • “Where is this endpoint used?” is one grep; invalidation can’t miss renamed
    keys; auth changes happen in one file.
  • New developers (and agents) extend tables instead of inventing plumbing.