Skip to content

API Contracts

@wavemap/api-contracts is the shared application boundary between Wavemap apps. It prevents the front end, back end, i18n package, and tests from each inventing their own names for the same route, response, query shape, permission, or domain code.

Use this page when adding or changing API routes, DTOs, API envelopes, query controls, page query presets, frontend route constants, roles, permissions, or domain-code surfaces that cross an app boundary.

The package exports one public entrypoint:

export * from "./constants"
export * from "./types"
export * from "./utils"

Its job is to publish shared contracts, not to implement app behavior.

SurfaceExamples
ConstantsBackend route constants, frontend route constants, role codes, permission codes, pagination limits.
DTOs and typesZod request/response schemas, inferred T... types, API envelopes, entity summary DTOs.
Query contractsFilter groups, filter clauses, sort instructions, endpoint capabilities, saved-view state schemas.
UtilitiesPermission derivation and query helper utilities that are contract-shaped rather than app runtime.

The package depends on @wavemap/shared-utils for lower-level shared primitives. Apps depend on @wavemap/api-contracts when they need the same route, data, or identity surface.

Route identity is split into composable layers:

  • constants/routing-common.ts owns shared path segments, parameter tokens, and unversioned backend route shapes.
  • constants/api/routes.v1.ts owns the versioned /api/v1/... route constants consumed by backend routes and frontend API callers.
  • constants/frontend/routes.ts owns frontend route constants such as artist, event, auth, profile, and dashboard paths.

Use route constants instead of handwritten strings in app code when a route is shared or repeated. Parameter tokens such as :artistID, :eventID, :mediaID, and :userID should come from the contract layer so path-building and tests do not drift independently.

Public entity routes should treat route identifiers as public IDs at the app boundary, even when a historical parameter name still says :artistID or similar. Backend handlers translate public IDs to internal database IDs before joins, authorization checks, and mutations. See Public Entity Identifiers for the durable publicId and slug convention.

AVAILABLE_BACKEND_ROUTES_V1 is the current route inventory for v1 backend routes. Add to it when a new route becomes part of the shared backend surface.

DTO files use Zod schemas for runtime validation and exported T... types for TypeScript consumers.

Public DTOs should not expose internal database IDs by default. Public entity responses should use publicId and, where helpful for canonical links, slug. Keep internal IDs inside server-side persistence, authorization, joins, audit targets, and explicitly internal/operational endpoints.

API responses should use shared envelopes:

  • APIEnvelopeOk(schema) for successful responses.
  • APIErrorEnvelope for structured API errors.
  • TAPIEnvelopeOk<TData> and TAPIEnvelopeError for static typing.

Error envelopes preserve both display fallback and stable identity:

  • message remains a fallback display string.
  • messageKey identifies localized backend user-facing copy when present.
  • messageInterpolationValues carries primitive ICU interpolation values for backend-localized errors.

When route behavior changes, keep the DTO, backend response validation, frontend parsing expectations, and tests aligned. Do not widen a DTO silently to match an accidental response shape.

Queryable collection pages should share applied query shapes through @wavemap/api-contracts.

The core query DTOs are:

  • QueryFilterGroupDTO
  • QueryFilterClauseDTO
  • QuerySortInstructionDTO

Endpoint capabilities describe what a page or route can expose:

  • Supported criteria.
  • Optional query keys.
  • Label keys for localized UI.
  • Sort defaults.
  • Filter data types and operations.
  • Pagination defaults, allowed limits, and max limit.

The frontend can hold richer draft state while a user edits filters, sorts, search, or saved views. The apply boundary should normalize that draft state into the shared contract before it reaches URL state, API calls, or persisted page query presets.

Saved-view contracts live in the same package because they cross frontend state, backend persistence, and schema-version compatibility. Page-specific preset DTOs should pin the page key and state schema so each page can evolve deliberately.

Role and permission identity belongs in @wavemap/api-contracts.

The package owns:

  • Role code constants.
  • Permission code constants.
  • rolesPermissionsMap.
  • Role and permission types derived from the available constants.

The back end uses this contract for route authorization, and the front end uses it for permission-aware rendering. Only role assignments are persisted; permissions are derived from roles at runtime.

Do not manually ask whether a user is an admin or root inside feature code when the question is really whether the current user has a permission. Use the shared permission vocabulary and the established auth helpers.

When adding permissions, prefer the narrowest stable action vocabulary that matches the user intent. Avoid collapsing association, media, profile, and ownership/control semantics into one broad update permission just because the same role receives every permission today.

Some API contract constants also feed i18n and database seed contracts.

Examples include:

  • Domain codes that need localized labels.
  • Page query preset page keys.
  • Filter, sort, operation, and data-type codes.
  • Link destination, status, and type code arrays.

When a source code value needs a localized label, keep the source array in code and let translation sheets render it. Translation files should not become the source of truth for allowed domain states.

When a code set also has backend base seeds, make sure seed files map over the canonical source array instead of hardcoding row values. pnpm verify:i18n checks the registered i18n domain contracts that are stable enough for continuous verification.

Keep these outside @wavemap/api-contracts:

  • Backend database queries, handlers, middleware, storage adapters, and email-sending behavior.
  • Frontend React components, page workflow state, TanStack Query hooks, and UI rendering.
  • Wavemap translation resources and runtime i18n loading.
  • Generic helpers that do not describe an API, route, query, permission, or domain-code contract.
  • Cloud resource names, runtime config, deploy workflow profiles, or operational command implementation.

If a helper is useful but not contract-shaped, consider @wavemap/shared-utils, an app-owned utility module, or @wavemap/operations depending on the audience.

When adding or changing a contract:

  1. Add or update constants and as const arrays before deriving union types.
  2. Add or update the Zod DTO when runtime validation matters.
  3. Export the contract through the relevant index.ts path.
  4. Update backend handlers, frontend API callers, and tests together.
  5. Update i18n domain contracts when a code value needs localized display.
  6. Update seed-source contracts when a backend base seed depends on the code set.
  7. Add tests for route inventories, DTO constraints, preset schema behavior, or helper behavior when the contract is non-obvious.

Prefer exact, deliberate changes. A shared contract can spread quickly through both apps, so avoid broad “while here” reshaping.

Useful commands:

Terminal window
pnpm -F @wavemap/api-contracts test
pnpm -F @wavemap/api-contracts typecheck
pnpm verify:i18n

Use targeted backend, frontend, or docs checks as needed when a contract change affects app behavior or documentation.