{
  "uid": "cogitave.learn.api-design.design-a-clean-api",
  "kind": "moduleUnit",
  "href": "/modules/api-design/design-a-clean-api/",
  "title": "Design a clean API",
  "summary": "",
  "type": null,
  "products": [],
  "roles": [],
  "levels": [],
  "subjects": [],
  "headings": [
    "1. Contract-first: the schema is the source of truth",
    "2. Model resources, then act on them",
    "3. The estate-wide invariants",
    "4. One shape, and the deviation rule"
  ],
  "source": "The [API design standard](../../../../standards/docs/standards/api-design.md)\nuses RFC 2119 keywords: **MUST/REQUIRED** is non-negotiable, **SHOULD/DEFAULT**\nis the opinionated default you ship unless you record a deviation, and **MAY** is\nallowed. Design a surface in this order.\n\n## 1. Contract-first: the schema is the source of truth\n\nThe contract artifact is authored **before** the implementation, and code is\ngenerated from - or verified against - it, never the reverse. OpenAPI 3.1 for\nREST, proto3 for gRPC, SDL for GraphQL, and JSON Schema per tool for MCP. The\nartifact lives in the repo and is the diff an auditor reads. Every surface a\nproduct exposes MUST **also** be reachable as MCP - the MCP projection is a\nfirst-class contract, not an afterthought. Contracts MUST be self-describing:\ndescriptions on every field and method, examples on every operation, so an agent\ncan discover a surface instead of regenerating it.\n\n## 2. Model resources, then act on them\n\nDefault to a **hierarchy of resources (nouns)** acted on by a small set of\nstandard methods; reach for an RPC-style verb only as a custom method. Paths\nalternate collection/id segments and express nesting explicitly, e.g.\n`/v1/work-items/{work_item_id}/comments/{comment_id}`. Naming has one canonical\ncasing per place:\n\n- URL collection segments: lower-kebab-case, **plural** (`/v1/work-items`).\n- JSON/proto fields: `snake_case` (`display_name`), so REST and gRPC never\n  diverge in casing.\n- Enum values: `UPPER_SNAKE_CASE` with a `*_UNSPECIFIED = 0` zero value.\n- MCP tool names: `snake_case` `verb_noun` (`create_work_item`).\n\nIDs are server-assigned, opaque, and sortable (ULID or UUIDv7); clients MUST NOT\nparse meaning out of them. **`List`/`Get`/`Create`/`Update`/`Delete`/`BatchGet`**\ncover CRUD - and nothing bespoke for it. Partial update via `PATCH` + a field\nmask is the default; a field absent from the mask MUST NOT be cleared. Non-CRUD\nactions (a state transition, a bulk job) become a **custom method** in colon\nnotation over `POST`: `POST /v1/work-items/{id}:archive`.\n\n## 3. The estate-wide invariants\n\nSome rules are MUST and **not deviable per repo**. Get these right or the design\nis wrong:\n\n- **Errors are RFC 9457 typed problems** on every surface -\n  `application/problem+json` with `type`, `title`, `status`, `detail`,\n  `instance`, plus the Cogitave extensions `code` (a stable error-code enum -\n  the machine contract), `trace_id`, `retryable`, and `errors[]` for field-level\n  validation. Each `code` maps to a canonical class and HTTP status. Raw panics\n  MUST NOT cross the boundary.\n- **Cursor pagination by default.** Every `List` takes `page_size` +\n  `page_token` and returns `next_page_token`, with an enforced default and\n  maximum page size (an unbounded list is a defect). The cursor is opaque and\n  signed; offset/limit is a recorded deviation.\n- **Idempotency on creating writes.** Every non-idempotent write honors an\n  `Idempotency-Key`: a replay returns the original response; a reused key with a\n  different fingerprint fails `409`.\n\n## 4. One shape, and the deviation rule\n\nA single concept MUST have **one shape** across REST, gRPC, GraphQL, and MCP -\nsame fields, enums, `code` taxonomy, and pagination. Divergence is a lint\nfailure, not a style choice; Spectral, `buf`, and schema linters gate it in CI.\nDeviating from a SHOULD/DEFAULT requires an **Accepted ADR/RFC** recording the\nreason, a compensating control, and an expiry date. The MUST invariants above are\nnot deviable at all.\n",
  "partOf": "cogitave.learn.api-design",
  "durationInMinutes": 8,
  "quiz": null
}