# Module: `src/api/endpoints.ts` > **Source**: `src/api/endpoints.ts` (79 lines) > **Topo batch**: B2 (leaf — no internal imports) > **Introduced**: AZ-486 (2026-05-11, commit `8a461a2`), closing architecture baseline finding F7. ## Purpose Single source of truth for every `/api//` URL the UI talks to. Replaces the hardcoded string literals that previously lived at each `api.*` / `createSSE` call site (and at every `src={...}` URL for API-served images / videos). The `endpoints` object is the canonical wire-contract documentation: each builder produces a character-identical string to the literal it superseded, so MSW handlers + e2e stubs + the nginx routing table all keep matching. Together with the `STC-ARCH-02` static gate (see [Configuration](#configuration)), this module enforces "no hardcoded API path literals in `src/`" as a build-time invariant rather than a code-review aspiration. ## Public interface ```ts export const endpoints = { admin: { authRefresh: () => string authLogin: () => string authLogout: () => string users: () => string user: (id: string) => string usersMe: () => string // added 2026-05-13 by AZ-510 — chained read after POST refresh classes: () => string class: (id: string | number) => string }, annotations: { classes: () => string settingsUser: () => string settingsSystem: () => string settingsDirectories: () => string annotations: () => string annotationsByMedia: (mediaId: string, pageSize?: number) => string // pageSize default = 1000 annotationImage: (annotationId: string) => string annotationThumbnail: (annotationId: string) => string annotationEvents: () => string media: (queryString: string) => string mediaFile: (mediaId: string) => string mediaItem: (mediaId: string) => string mediaBatch: () => string dataset: (queryString: string) => string datasetItem: (annotationId: string) => string datasetBulkStatus: () => string datasetClassDistribution: () => string }, flights: { collection: (queryString?: string) => string // GET ?pageSize=... lists; POST (no qs) creates aircrafts: () => string aircraft: (id: string) => string flight: (id: string) => string flightWaypoints: (id: string) => string flightWaypoint: (flightId: string, waypointId: string) => string flightLiveGps: (id: string) => string }, detect: { media: (mediaId: string) => string // POST → trigger detection for a media item }, } as const ``` The whole object is `as const`, so each leaf's return type is the narrow string literal where possible (e.g. `'/api/admin/auth/refresh'`) and the parameterised builders carry a `string` return. ## Internal logic - **Pure data + template strings.** No side effects, no I/O, no caching. Every builder is a one-line `() => '...'` or arrow returning a template literal. - **Function form (not constants)**, per direction at task-creation time: - Parameterised paths (e.g. `flight(id)`) need a function anyway. Keeping every entry as a function — even the constant ones — gives a single uniform call shape at every site (`endpoints.x.y()`) so reviewers don't have to remember which entries take parens and which don't. - Per-builder tree-shaking under Vite's production rollup remains intact. - **Query strings owned by the caller for variable-shape paths.** Where the query is dynamic (`flights.collection`, `annotations.media`, `annotations.dataset`), the caller builds a `URLSearchParams.toString()` and the builder owns only the path + `?`. This keeps the wire contract identical to pre-refactor literals at every callsite. ## Public API (barrel re-export) `src/api/index.ts` re-exports `endpoints` alongside `api`, `createSSE`, `setToken`, `getToken`, `getApiBase`, `setNavigateToLogin`. Consumers OUTSIDE the `01_api-transport` component MUST import from the barrel (`import { endpoints } from '@/api'` or `from '../api'`) — direct imports of `src/api/endpoints` from other components are blocked by `STC-ARCH-01` (F4 closure, see `src__api__client.md`). ## Dependencies - **Internal**: none. - **External**: none. ## Consumers (intra-repo) After the AZ-486 migration, `endpoints` is imported by: - `src/api/client.ts` — internal `refreshToken()` helper uses `endpoints.admin.authRefresh()`. - `src/auth/AuthContext.tsx` — `authRefresh`, `authLogin`, `authLogout`, `usersMe` (added by AZ-510). - `src/components/FlightContext.tsx` — `flights.collection`, `flights.flight`, `annotations.settingsUser`. - `src/components/DetectionClasses.tsx` — `admin.classes`, `admin.class`. - `src/features/admin/AdminPage.tsx` — `admin.users`, `admin.user`. - `src/features/annotations/AnnotationsPage.tsx` — annotation CRUD endpoints, `detect.media`. - `src/features/annotations/AnnotationsSidebar.tsx` — `annotations.annotationEvents` (SSE), bulk-status, dataset endpoints. - `src/features/annotations/CanvasEditor.tsx` — `annotations.annotationImage`, `annotations.annotationThumbnail`. - `src/features/annotations/MediaList.tsx` — `annotations.media`, `annotations.mediaFile`, `annotations.mediaItem`, `annotations.mediaBatch`. - `src/features/annotations/VideoPlayer.tsx` — `annotations.mediaFile`. - `src/features/dataset/DatasetPage.tsx` — `annotations.dataset*` family, `annotations.classes`, `annotations.annotationImage`. - `src/features/flights/FlightsPage.tsx` — full `flights.*` surface + `annotations.settingsUser`. - `src/features/settings/SettingsPage.tsx` — `annotations.settings*`, `flights.aircrafts`. This is the full intra-repo consumer list — `STC-ARCH-02` guarantees no production-source caller falls outside it. ## Data models None defined here. Path-string output only. ## Configuration The module IS the API-path configuration. The only "config" is the nginx routing table that maps each `/api//...` prefix to a concrete backend service — see `src__api__client.md` → External integrations for the live table. **Static enforcement (`STC-ARCH-02`)**: - **Script**: `scripts/check-arch-imports.mjs --mode=api-literals`. - **Wired into**: `scripts/run-tests.sh` (functional profile, static group) — runs before any unit test. - **What it forbids**: any `/api//...` literal in `[`'"]` quoting under `src/`. - **Exempt files**: this file (`src/api/endpoints.ts`) and `src/**/*.test.ts(x)` only. - **Bypass policy**: none. Adding a new exempt path requires updating the exempt regex in the script AND a `module-layout.md` rule revision in the same commit. ## External integrations This module integrates nothing directly. It documents — as TypeScript values — the wire contract for every external integration the SPA has, as routed by `nginx.conf`. See the routing table in `src__api__client.md` → External integrations for the per-prefix backend mapping. ## Security - **No bearer plumbing here.** Token injection still happens in `client.ts` (`Authorization` header) and `sse.ts` (`access_token` query parameter). Builders return URLs **without** the token. - **No URL-encoding** of interpolated `id` / `mediaId` / `queryString` parameters. All current callsites pass already-safe values (UUIDs, ints, pre-built `URLSearchParams.toString()` output). If any future caller passes user-controlled text, the builder must add `encodeURIComponent` (see open question below). - **No CSRF surface change** — same posture as the pre-refactor literals. ## Tests - **`src/api/endpoints.test.ts`** (36 Vitest assertions): pins every builder's output to its exact pre-refactor URL string. This is the contract documentation — any wire-contract change MUST update this test in the same commit as the backend / MSW / e2e stub change. Includes one barrel-re-export assertion (`endpoints` is reachable via `import { endpoints } from '../api'`). - **`tests/architecture_imports.test.ts`** (AZ-486 / STC-ARCH-02 suite, 6 cases): verifies the static gate passes on the migrated codebase AND fails when a synthetic single-quoted / double-quoted / template-literal `/api//...` literal is introduced in `src/`. Also verifies the `*.test.ts` and `//` comment exemptions. ## Notes / open questions - **`detect.media` only exposes the single-segment path** that the UI uses today (`POST /api/detect/`). The full `detect/` service has more endpoints (per the nginx table) but no UI callsite consumes them. Add new builders only when a real callsite needs them — don't pre-populate. - **`flights.collection` overloads its return** on whether `queryString` is provided. Acceptable while the contract is "GET with `?pageSize`, POST without" — but if a third flights-collection verb (DELETE? PUT?) is ever added with its own query shape, split into named builders rather than threading more conditional logic through one. - **No URL-encoding of interpolated params** (see Security). Add `encodeURIComponent` at the first callsite that needs it, plus a contract-test case in `endpoints.test.ts`. Currently safe across all 36 pinned URLs. - **Wire-contract test coverage is exact-string, not shape.** This is deliberate: a "looks like a path" matcher would silently accept a hyphen-to-underscore change that breaks the backend. Updating these strings IS a wire-contract change — treat the test as a release-gate.