Skip to main content

API Conventions

One page of rules shared by every module, so the per-module docs stay short.

Base URLs

  • Backend origin: https://shanebackend-production.up.railway.app
  • https://shanejli.com/api/* rewrites to the same backend (afterFiles rewrite), so relative /api/... URLs work from the site's own pages.

Errors

Every error is JSON {"error": "message"}. Validation failures (bad params, query, or body) return 400 with the zod error object. Unhandled throws return 500 {"error":"Internal server error"}.

Auth posture

  • Missing/invalid token on a protected route: 401.
  • PAT lacking the required scope: 403 Token missing required scope: <scope>.
  • A row that exists but is not yours: usually 404, not 403 (deliberately indistinct; loans is the exception and 403s).

Wire format

  • JSON bodies. Request field names are camelCase in most modules; the journal and comment surfaces use snake_case for multi-word request fields (parent_comment_id, base_version_num, target_version_num). Responses are camelCase everywhere.
  • Dates that key resources are YYYY-MM-DD strings, validated as real calendar dates (2026-02-30 is a 400, not a 500).
  • Free-text fields are trimmed before validation; whitespace-only input is a 400.

Pagination

Keyset, newest-first, via limit (1 to 100) and cursor. The cursor value differs by module: the journal cursor is the last entry's DATE (YYYY-MM-DD); trips, loans, tea, scoreboard matches, and rng history use a createdAt ISO timestamp. Read nextCursor from each response; null means done.

Optimistic concurrency (If-Match)

Racy journal mutations (revert, suggestion approve) require an If-Match header carrying the entry's current version number as a plain integer (from GET /api/journal/entries/:date, field currentVersionNum).

  • Missing header: 428
  • Non-numeric: 400
  • Stale: 409 with {"error":"Version conflict","currentVersionNum":<latest>} so you can rebase and retry.

Rate limits

Per-PAT rolling 60 second buckets (JWT browser sessions bypass); see the bucket table in Auth and Tokens. 429 responses carry Retry-After.

CORS

Allowed request headers are Content-Type, Authorization, If-Match, X-Tea-Pin. A new custom header needs a backend change; the symptom of forgetting is a browser-only "Failed to fetch".