Skip to content

API conventions

The wire-format policy for the GeoMetrikks HTTP API, decided for 0.7.0. tests/test_error_contract.py and the generated OpenAPI document (resources/generated/openapi.json) pin these rules; change them only with a coordinated client regeneration and a changelog migration note.

Every REST endpoint lives under /api/v1, mounted as a single Litestar Router in geometrikks/server/routes.py, which stays the one explicit registration point. Controllers live in their vertical domain packages (geometrikks/domain/<domain>/controllers*) and own only their domain segment (/analytics, /crowdsec, …); the router supplies the version prefix. Everything under /api/v1 requires the session cookie except /api/v1/auth/login (with APP_AUTH_DISABLED=true nothing requires a session, and the auth endpoints stay registered as mode-appropriate no-ops: /api/v1/auth/me and a valid /api/v1/auth/login return {"mode": "disabled"}, and /api/v1/auth/logout returns 204). Outside the router:

  • /health and /health/ready: unauthenticated probe endpoints.
  • /ws/live, /ws/logs, /ws/crowdsec: WebSocket feeds (geometrikks/domain/realtime/), session-authenticated during the handshake.
  • /schema (deliberately unauthenticated), /sw.js, and the SPA shell.

All request and response JSON fields are camelCase on the wire; Python attributes stay snake_case.

  • SQLAlchemy-model responses use Advanced Alchemy DTOs with rename_strategy="camel".
  • Bespoke request and response models are msgspec Structs declared with rename="camel" (see geometrikks/domain/geo/schemas.py for the idiom); they live in each domain package’s schemas.py/dtos.py or next to their controller. Digit-adjacent names pin their wire form explicitly with msgspec.field(name=...): status2xx, requestCount24h.
  • Query parameters carry explicit camelCase name= declarations (fromTimestamp, startDate, ipAddressNotIn, …); shared aliases live in geometrikks/lib/parameters.py.
  • Path parameters are URL segments, not fields, and stay snake_case ({location_id}, {job_id}).
  • WebSocket frame payloads are a separate contract and are not covered by this policy (revisited with the realtime refactor).
  • Data values are exempt: SettingFieldView.key deliberately carries Python settings field names like home_latitude, and /api/v1/logs/tail records pass raw structlog context keys through as recorded (request_id, status_code, …); renaming historical log data would misrepresent it. OpenAPI documents only the stable record fields.

Errors use Litestar’s native HTTP-exception envelope, unchanged:

{"status_code": 404, "detail": "Not Found"}
  • extra appears additionally on request-validation errors with the per-field breakdown.
  • The envelope keys are the framework’s and stay snake_case; the camelCase policy applies to success payloads only.
  • Domain exceptions (DomainValidationError, CrowdSec errors) are translated centrally in geometrikks/server/exceptions.py into the same envelope.
  • API-path 404s render this envelope too; non-API 404s (static-asset misses) keep litestar-vite’s empty-body behavior. The 404 handler treats the whole /api/ namespace as API surface, matching the auth boundary: an unknown or unversioned /api/ path is an API consumer’s mistake and gets JSON.

OpenAPI operation IDs use Litestar’s default path-derived naming (ApiV1AnalyticsSummaryGetSummary). Generated TypeScript client method names hang off them, so route moves must keep full paths stable or accept a client-wide rename.