{"slug":"api-design-standards","name":"API Design Standards","kind":"knowledge","version":1,"format_version":1,"checksum":"a7401977351d12ee3b9e660f73011dbb8fb99eebf7b2cd6aa5915b85f15d25bd","bundle":{"edges":[{"label":"errors are part of the contract","edge_type":"related","sort_order":0,"is_directed":false,"source_slug":"rest-conventions","target_slug":"api-errors"},{"label":"additive change beats a new version","edge_type":"related","sort_order":1,"is_directed":false,"source_slug":"api-versioning","target_slug":"rest-conventions"},{"label":"list endpoints share one shape","edge_type":"related","sort_order":2,"is_directed":false,"source_slug":"api-pagination","target_slug":"rest-conventions"}],"pages":[{"body":"# Resource naming and HTTP semantics\n\n## Paths\n- Plural nouns for collections: `/listings`, `/listings/{slug}`. No verbs in paths — the method IS the verb.\n- Nesting expresses ownership and stops at two levels; deeper relationships flatten into query filters.\n- Path segments are stable identifiers (ids, slugs), never display names.\n\n## Methods\n- GET reads, never mutates — a GET with side effects breaks caches, crawlers, and retries.\n- PUT replaces and is idempotent; PATCH edits the fields provided; POST creates or triggers, and is the only non-idempotent verb you get.\n- DELETE is idempotent: the repeat call returns the same result, and a soft delete still answers 200/204 — not 404 — on the repeat.\n\n## Status codes\n- 200/201/204 for success (201 with a Location header on create).\n- 400 malformed request · 401 unauthenticated · 403 unauthorized · 404 missing OR hidden (never reveal which) · 409 conflict · 422 parsed but failed validation.\n- 5xx means WE broke — a client's bad input must never surface as a 500.\n","icon":"🧭","title":"Resource naming and HTTP semantics","category_slugs":["rest-conventions"]},{"body":"# One error shape everywhere\n\nClients write their error handling once, against one envelope:\n\n    {\"detail\": {\"code\": \"listing_not_found\",\n                \"message\": \"No listing with that slug.\",\n                \"field_errors\": {\"slug\": \"unknown\"}}}\n\n## The rules\n- `code` is a stable machine-readable slug — clients branch on it, so renaming one is a breaking change.\n- `message` is human-readable and safe to display verbatim: no internals, no stack traces, no SQL, no hostnames.\n- `field_errors` (optional) maps input fields to per-field problems, so forms can highlight the actual field.\n\n## House rules\n- 400 = the request couldn't be parsed; 422 = it parsed but failed validation. Pick per the boundary, not per the mood.\n- Validation reports ALL failing fields in one response — clients shouldn't fix errors one round-trip at a time.\n- The error shape is part of the API contract: it appears in the docs, it has tests, and it never varies by endpoint.\n","icon":"🚨","title":"One error shape everywhere","category_slugs":["api-errors"]},{"body":"# Versioning and compatibility rules\n\nThe cheapest version is the one you never have to mint.\n\n## Additive first\n- New capability arrives as new OPTIONAL fields and new endpoints — never by changing the meaning of an existing field.\n- Never repurpose or re-type a field: a field name, once shipped, keeps its meaning forever. Need a new meaning? Add a differently-named field that says what it means.\n- Readers are tolerant: unknown fields in a response are ignored, never an error — that's what lets the API grow without breaking old clients.\n\n## When you truly must break\n- A major version is a LAST resort: path-level (`/v2/...`), whole-surface, with both versions served for a published deprecation window.\n- Deprecations are announced in the changelog, marked in the docs, and signaled in responses (a deprecation header) — silence is how clients get broken \"without warning\" that was warned about nowhere.\n\n## The test\nBefore merging a schema change, ask: does yesterday's client still work against today's API without edits? If no, it's a break — version it or redesign it.\n","icon":"🧬","title":"Versioning and compatibility rules","category_slugs":["api-versioning"]},{"body":"# Pagination, filtering, and sorting\n\nEvery list endpoint shares one envelope:\n\n    {\"items\": [...], \"total\": 123, \"page\": 1, \"page_size\": 24}\n\n## Pagination\n- `page` is 1-based; `page_size` has a default AND a hard cap (the server clamps, it doesn't error) — an uncapped page size is a self-DoS endpoint.\n- `total` is the FILTERED count, so clients can render page controls.\n\n## Filtering\n- Filter params are named after the fields they filter: `?kind=bloom`, `?status=published`. Free-text search is always `q`.\n- Unknown filter values return an empty page, not an error — filters compose, and a picky one breaks the combination.\n\n## Sorting\n- `sort` takes named options (`updated`, `newest`, `downloads`) documented per endpoint — not raw column names, which leak schema and invite injection.\n- Every sort has a deterministic tiebreaker (id) so pagination never shows a row twice or drops one at a page boundary.\n","icon":"📄","title":"Pagination, filtering, and sorting","category_slugs":["api-pagination"]}],"format":"bloom.knowledge_bundle","version":1,"bindings":[],"categories":[{"icon":"🧭","name":"REST Conventions","slug":"rest-conventions","color":"#818cf8","sort_order":0,"description":"Resource naming, HTTP method discipline, and status-code semantics.","parent_slug":null,"default_mode":"enforce"},{"icon":"🚨","name":"Error Contract","slug":"api-errors","color":"#fb7185","sort_order":1,"description":"One machine-readable error envelope, everywhere, with messages that never leak internals.","parent_slug":null,"default_mode":"enforce"},{"icon":"🧬","name":"Versioning & Compatibility","slug":"api-versioning","color":"#34d399","sort_order":2,"description":"Additive-first evolution, tolerant readers, and what actually justifies a /v2.","parent_slug":null,"default_mode":"follow"},{"icon":"📄","name":"Pagination & Filtering","slug":"api-pagination","color":"#a78bfa","sort_order":3,"description":"One list envelope, capped page sizes, stable sorts, and filter params named after fields.","parent_slug":null,"default_mode":"follow"}],"exported_at":"2026-07-17T00:00:00Z","source_organization_id":null}}