Skip to main content

API Path Conventions — Ghasi Melmastoon

Companion: NAMING.md · ERROR_CODES.md · CODING_STANDARDS.md · docs/05-api-design.md · DEFINITION_OF_DONE.md

This is the single, authoritative URL grammar for every HTTP route under ghasi-melmastoon — every microservice API, every BFF, every webhook, every sync endpoint. The OpenAPI diff gate enforces it. The code-reviewer subagent flags violations.


1. URL grammar

/api/v<MAJOR>/<resource-plural-kebab>[/<id>][/<sub-resource-plural-kebab>][/<id>][/<verb-kebab>]
TokenRuleExample
/apiRequired prefix for service APIs (direct microservice)./api/v1/reservations
/bff/<surface>Required prefix for BFF routes; replaces /api. Surface ∈ consumer, tenant-booking, backoffice./bff/tenant-booking/v1/quotes
/sync/v<MAJOR>Required prefix for sync engine routes (desktop ↔ cloud)./sync/v1/pull
/webhooks/<vendor>Required prefix for inbound webhooks (one per vendor)./webhooks/stripe
/health, /ready, /metricsReserved single-segment liveness/readiness/metrics endpoints./health
v<MAJOR>Major version, integer ≥ 1. Never v1.0, never v1beta.v1, v2
<resource-plural-kebab>Plural noun, kebab-case, lower-case. Match an entity in the owning service's DOMAIN_MODEL.md.reservations, key-credentials, rate-plans, room-types
<id>ULID with the registered prefix from NAMING.md §ID prefixes.rsv_01H..., key_01H...
<verb-kebab>Past or present-tense verb in kebab-case. Used only for non-CRUD actions on a resource (e.g., check-in, cancel, refund, revoke)./reservations/{id}/check-in

Constraints:

  • Lowercase only. No camelCase, no snake_case, no UPPERCASE.
  • No trailing slashes.
  • No file extensions in the URL (.json, .xml forbidden — use the Accept header).
  • No query parameters used as routing (use a path segment).
  • Maximum URL depth: 6 segments after the version prefix. If you need more, model it as a separate resource.
  • IDs always carry the prefix; never bare ULIDs in URLs.

2. HTTP method semantics

MethodSemanticsIdempotentBodyResponse
GET /<resource>ListYesNone200 + cursor-paginated DTO list
GET /<resource>/{id}ReadYesNone200 + DTO; 404 + canonical error if absent
POST /<resource>CreateIdempotent via header (see §6)Zod-validated DTO201 + DTO; Location header to the new resource
PUT /<resource>/{id}ReplaceYesZod-validated DTO (full)200 + DTO; 404 + canonical error if absent
PATCH /<resource>/{id}Partial updateIdempotent via headerRFC 7396 merge-patch DTO200 + DTO
DELETE /<resource>/{id}DeleteYesNone204; 404 + canonical error if absent
POST /<resource>/{id}/<verb>Domain actionIdempotent via headerAction-specific Zod DTO200 + DTO (or 202 if async)

No GET with a body. No DELETE with a body. No verbs in GET URLs.


3. Resource-vs-action decision

A new endpoint is a resource if it represents a "thing" with a lifecycle (create / read / update / delete). It is an action if it represents an irreversible state transition on an existing resource.

Looks likeIsPath
Confirm a reservationAction on reservationPOST /api/v1/reservations/{id}/confirm
Cancel a reservationAction on reservationPOST /api/v1/reservations/{id}/cancel
Refund a paymentAction on paymentPOST /api/v1/payments/{id}/refund
Issue a key credentialResource (lifecycle)POST /api/v1/key-credentials
Revoke a key credentialAction on key credentialPOST /api/v1/key-credentials/{id}/revoke
Submit theme for reviewAction on themePOST /api/v1/themes/{id}/submit-for-review
Publish a themeAction on themePOST /api/v1/themes/{id}/publish
Search propertiesResource collection (the search hits)GET /api/v1/searchable-documents?… (search-aggregation)

When in doubt, prefer resource. Verb URLs proliferate fast and become hard to evolve.


4. Versioning rules

  • Every route is versioned with /v<MAJOR> from day one.
  • A non-breaking change (additive field, new optional field, new endpoint) ships in the same major version.
  • A breaking change (removed field, type change, semantics change, narrowed enum) requires a new major version:
    • Mount the new version side-by-side: /api/v1/... and /api/v2/....
    • Add Deprecation: true and Sunset: <RFC 1123 date> headers to v1 responses.
    • Sunset window: ≥ 90 days for service↔service routes, ≥ 180 days for BFF↔frontend routes.
    • Track deprecation in services/<name>/API_CONTRACTS.md deprecation table.
  • The OpenAPI diff gate compares the committed openapi.json against the previous commit. Breaking diffs without a major bump fail CI.

5. Pagination, filtering, sorting

Query parameterTypeDefaultNotes
cursoropaque string (base64-encoded ULID + filter hash)emptyForward-only. No offset. Reverse pagination is a separate parameter before.
limitinteger25Max 100. Beyond that, return 400 MELMASTOON.GENERAL.PAGINATION_LIMIT_EXCEEDED.
sortcomma-separated field or -fieldresource-definedAllowed fields are documented in API_CONTRACTS.md. Reject unknown fields with 400.
filter[<field>]scalar or comma-listnoneOne filter per field. Multi-value = OR. Multiple filters = AND.
qstringnoneFree-text query (only for search endpoints).
includecomma-list of related resourcesnoneSparse hydration; documented per route.

Response envelope for collections:

{
"data": [ { ... } ],
"page": {
"cursor": "eyJpZC…",
"next": "eyJpZC…",
"prev": null,
"limit": 25,
"hasMore": true
}
}

Single-resource responses are flat (no data envelope) so the OpenAPI types are clean: Reservation not { data: Reservation }.


6. Idempotency

Every write endpoint (POST, PUT, PATCH, DELETE) accepts Idempotency-Key as a request header.

  • Format: 16–64 ASCII chars; ULID is recommended.
  • Server stores (tenant_id, route, idempotency_key) → response for 24 h.
  • A duplicate request returns the stored response with the original status code, headers, and body.
  • A duplicate with a different body returns 409 MELMASTOON.GENERAL.IDEMPOTENCY_CONFLICT.
  • The header is mandatory on the consumer-facing BFF + the cash-in-transit endpoints (payments, lock issuance, refunds). It is optional but recommended elsewhere.
  • Outbox writes use the idempotency key as the message dedupe key downstream.

7. Tenant context

Every authenticated route requires:

  • Authorization: Bearer <jwt> — JWT carries tid (tenant ID), sub (user ID), mid (membership ID).
  • X-Tenant-Id: tnt_<ulid> — explicit header.
  • The TenantContextGuard cross-checks header.X-Tenant-Idjwt.tid. Mismatch → 403 MELMASTOON.SECURITY.TENANT_MISMATCH.
  • The platform-admin (platform_admin role) may pass X-Acting-Tenant-Id to override; logged with actor_role=platform_admin for audit.

Public, unauthenticated routes (consumer meta-search, public booking quote) accept neither header. They derive context from the URL (e.g., /bff/tenant-booking/v1/quotes resolves the tenant from the host or path).


8. Headers (standardized)

HeaderDirectionPurpose
AuthorizationRequestJWT bearer (see §7).
X-Tenant-IdRequestExplicit tenant context (see §7).
X-Request-IdRequest, optional; mirrored in responseCorrelation. Generated server-side if absent.
Idempotency-KeyRequestSee §6.
traceparent, tracestateBidirectionalW3C Trace Context. Always propagated.
Accept-LanguageRequestRFC 5646 BCP-47 list; resolves locale via fallback chain in tenant.localeFallback.
AcceptRequestapplication/json only. Other types → 406.
Content-TypeRequest (writes)application/json or application/merge-patch+json (PATCH).
If-MatchRequest (PUT/PATCH)ETag for optimistic concurrency. Mismatch → 412 MELMASTOON.GENERAL.PRECONDITION_FAILED.
ETagResponseStrong ETag based on aggregate version + content hash.
Last-ModifiedResponseRFC 1123 timestamp.
Sunset, DeprecationResponse (deprecated routes)RFC 8594 + draft Deprecation header.
RateLimit-Limit, RateLimit-Remaining, RateLimit-ResetResponseRFC 9440.
X-ServiceResponseOwning service name (audit + debugging).
X-API-VersionResponseEchoed major version for clients that didn't introspect the URL.

9. Status codes

CodeWhen
200 OKSuccessful read or update.
201 CreatedSuccessful create; Location header points to the new resource.
202 AcceptedAsync work queued; body includes a jobId and a Location for polling.
204 No ContentSuccessful delete or action with no body.
400 Bad RequestValidation failure; body is the canonical error envelope.
401 UnauthorizedMissing or invalid JWT.
403 ForbiddenAuthenticated but not authorized for this tenant or this resource.
404 Not FoundResource does not exist (or does not exist for this tenant — RLS hides cross-tenant existence).
409 ConflictIdempotency conflict, optimistic-lock conflict, business-rule conflict (e.g., overbooking).
410 GoneResource was deleted; do not retry.
412 Precondition FailedIf-Match ETag mismatch.
415 Unsupported Media TypeWrong Content-Type.
422 Unprocessable EntityValidation passed but business rule rejected (use sparingly; prefer 409 for state conflicts).
429 Too Many RequestsRate-limit exceeded; RateLimit-* headers + Retry-After.
500 Internal Server ErrorUnexpected server failure; never exposes stack trace.
502 Bad GatewayUpstream third-party failed (lock vendor, payment vendor); body documents which one.
503 Service UnavailableDownstream dependency degraded; Retry-After set.
504 Gateway TimeoutUpstream timeout.

10. Error response envelope (always identical)

{
"error": {
"code": "MELMASTOON.RESERVATION.OVERBOOKING_BLOCKED",
"message": "Cannot confirm reservation: no inventory hold is attached.",
"details": {
"reservationId": "rsv_01H...",
"roomTypeId": "rmt_01H..."
},
"requestId": "req_01H...",
"timestamp": "2026-04-23T10:11:12.345Z",
"documentation": "https://docs.ghasi.tech/errors/MELMASTOON.RESERVATION.OVERBOOKING_BLOCKED"
}
}
  • code — canonical, machine-readable, registered in ERROR_CODES.md.
  • message — human-readable English; localized client-side via the code → translation map.
  • details — optional, schema-documented per code.
  • requestId — always present, equal to X-Request-Id of the response.
  • timestamp — ISO 8601 UTC.
  • documentation — optional URL to public error docs (auto-generated from the registry).

Never include stack traces, raw SQL, raw third-party error payloads, PII, or secrets in details.


11. BFF route conventions

BFFs (bff-consumer-service, bff-tenant-booking-service, bff-backoffice-service) follow the same grammar with these additions:

  • Prefix is /bff/<surface>/v<MAJOR> instead of /api/v<MAJOR>. <surface>consumer, tenant-booking, backoffice.
  • BFF routes may compose multiple downstream service responses into a single screen-shaped DTO. Document the composition in API_CONTRACTS.md.
  • BFF routes may be screen-scoped (/bff/backoffice/v1/screens/arrivals-board) when the response is the data for one specific screen. This is allowed; document under API_CONTRACTS.md.
  • BFFs never expose internal service shapes. Always remap to @ghasi/api-contracts BFF types.
  • BFFs always carry session cookies for browser surfaces; mobile uses Authorization bearer.

12. Sync engine routes

The desktop ↔ cloud sync engine has its own grammar:

  • POST /sync/v1/pull — body declares cursor per aggregate; response is a delta + new cursor + freshness watermark.
  • POST /sync/v1/push — body is an outbox batch; response is per-record accepted / conflict / rejected.
  • POST /sync/v1/handshake — device binds, exchanges keys, declares schema version.
  • POST /sync/v1/heartbeat — keep-alive + clock skew measurement.

Detailed contract in services/<name>/SYNC_CONTRACT.md per service. The cross-service shape lives in @ghasi/sync-protocol.


13. Webhook conventions

Inbound webhooks (Stripe, PayPal, MFS providers, lock vendors):

  • Path: /webhooks/<vendor>. One path per vendor.
  • Signature verification mandatory, before any other parsing. Failure → 401.
  • Idempotent processing. Vendor's event ID is the dedupe key.
  • Always respond 200 quickly (≤ 500 ms) and process async.
  • Vendor-specific request shapes are documented in services/<name>/API_CONTRACTS.md under "Webhooks".

Outbound webhooks (we send to tenants): not in R1; will follow this same grammar when introduced.


14. Liveness, readiness, metrics

PathPurposeAuthBody
/healthLiveness probe (process is up)None{ "status": "ok" }
/readyReadiness probe (deps reachable: DB, Pub/Sub, downstream services)None{ "status": "ok" or "degraded", "checks": [...] }
/metricsPrometheus scrapeInternal-only (network policy)OpenMetrics text

These three paths are the only single-segment routes allowed.


15. Examples (canonical, copy these patterns)

GET /api/v1/reservations?filter[status]=confirmed&sort=-createdAt&limit=50
GET /api/v1/reservations/rsv_01H.../folio
POST /api/v1/reservations (Idempotency-Key, X-Tenant-Id)
PATCH /api/v1/reservations/rsv_01H... (If-Match, Idempotency-Key)
POST /api/v1/reservations/rsv_01H.../check-in (Idempotency-Key)
POST /api/v1/reservations/rsv_01H.../cancel (Idempotency-Key)

POST /api/v1/key-credentials (Idempotency-Key)
POST /api/v1/key-credentials/key_01H.../revoke (Idempotency-Key)

POST /api/v1/payments/pay_01H.../refund (Idempotency-Key)

GET /api/v1/properties/ppt_01H.../rooms?filter[status]=available
GET /api/v1/properties/ppt_01H.../rate-plans

POST /bff/tenant-booking/v1/quotes (no auth; tenant resolved from host)
POST /bff/tenant-booking/v1/quotes/quo_01H.../hold
POST /bff/tenant-booking/v1/bookings (Idempotency-Key)

GET /bff/backoffice/v1/screens/arrivals-board?date=2026-04-23
POST /bff/backoffice/v1/key-credentials/key_01H.../revoke

POST /sync/v1/pull
POST /sync/v1/push
POST /sync/v1/handshake

POST /webhooks/stripe
POST /webhooks/paypal
POST /webhooks/ttlock

GET /health
GET /ready
GET /metrics

16. Anti-patterns (auto-flagged)

  • /api/v1/getReservations (verb in URL).
  • /api/v1/reservation/{id} (singular).
  • /api/v1/reservations/{id}/folio.json (extension).
  • /api/v1/reservations?id=rsv_... (query as routing).
  • /api/reservations (no version).
  • /v1/api/reservations (wrong order).
  • GET /api/v1/reservations/{id}/cancel (action on GET).
  • POST /api/v1/reservations/cancel?id=rsv_... (id in query for an action).
  • /api/v1/reservations/cancel-by-guest (multi-verb).
  • 200 + { error: ... } (use the right status).
  • Unbounded list responses (no limit, no cursor).
  • Returning entity with internal fields (e.g., _internalSnapshot, __v).
  • Inconsistent envelope (some routes wrap in data, some don't, on the same service).

17. Cross-references


18. Versioning of this document

Same governance as CODING_STANDARDS.md §19. Loosening any rule (e.g., allowing offset pagination) requires an ADR.