API Design Standards
An API is a promise to callers you don't control and usually can't see. Adding to that promise is free; changing or removing part of it breaks someone who already depended on it. Every API change is classified against that promise (additive or breaking) before it ships, not discovered after a caller files a bug.
Phase 1: Resource and URL conventions
- URLs name resources (nouns), never actions (verbs):
POST /orders, not POST /createOrder. The HTTP method carries the verb.
- Collections are plural (
/orders), a specific member is the collection plus an identifier (/orders/{id}), and nesting reflects real ownership (/orders/{id}/items), not an arbitrary path someone found convenient.
- HTTP methods map to a fixed meaning, and only that meaning:
GET (read, no side effects, safe to retry and cache), POST (create, or a non-idempotent action), PUT (full replace, idempotent, calling it twice with the same body leaves the same state), PATCH (partial update), DELETE (remove, idempotent).
- Status codes communicate outcome precisely:
2xx success (201 for created with a Location header, 204 for success with no body), 4xx the caller's fault (400 malformed request, 401 unauthenticated, 403 authenticated but not authorized, 404 not found, 409 conflict, 422 semantically invalid), 5xx the server's fault. Returning 200 with an error payload embedded in the body hides the failure from every generic HTTP-aware tool (retries, caches, monitoring).
Phase 2: Versioning strategy
Pick one signal and use it consistently across the whole API; mixing strategies forces every client to handle more than one:
| Strategy |
How |
Tradeoff |
URI path (/v2/orders) |
version segment in the path |
most discoverable and cacheable, but encourages whole-API version bumps for a change that only touched one resource |
Header / content negotiation (Accept: application/vnd.api+json;version=2) |
version encoded in a request header |
most correct REST semantics (a version is a different representation of the same resource, not a different resource) but far less discoverable: undebuggable by just reading a URL |
Query parameter (?version=2) |
version as a query param |
simplest to add after the fact, easiest to accidentally omit and silently get a default |
Version the API only when a breaking change (Phase 3) actually requires it. A version bump for a purely additive change forces every existing client to migrate for no reason; that cost is the same mistake a hand-picked SemVer bump is, applied to an API instead of a package.
Phase 3: What's backward-compatible vs breaking
| Change |
Compatible? |
| Adding a new optional request field |
Compatible: old clients that don't send it are unaffected |
| Adding a new field to a response |
Compatible if clients are expected to ignore unknown fields: state this expectation explicitly in the API's docs, since some client generators fail closed on unknown fields |
| Adding a new endpoint |
Compatible |
| Removing or renaming a field, endpoint, or enum value |
Breaking |
| Changing a field's type or meaning (a string that used to be an ID now being a display name) |
Breaking |
| Making a previously-optional field required |
Breaking |
| Changing default behavior for an omitted parameter |
Breaking |
| Tightening validation on an existing field (was permissive, now rejects previously-valid values) |
Breaking |
When a breaking change is unavoidable (security fix, correcting a genuinely wrong design): communicate it ahead of the change, support the old and new shape in parallel for a stated deprecation window (see dependency-upgrade-management for the deprecation-signal mechanics), and only remove the old shape after that window closes.
Phase 4: Contract testing
- An OpenAPI (or equivalent) schema is the API's published, machine-readable shape; validate requests and responses against it in CI so a gross schema violation is caught before it reaches a client.
- For APIs consumed by services you also control (internal microservices), add consumer-driven contract testing: the consumer writes a test capturing exactly what it expects from the provider, the provider verifies against that captured contract in its own CI. This catches a breaking change (a renamed field, a removed endpoint) at the point the provider would have shipped it, not after deploy, in the consumer's production logs.
- Run the schema validation and the contract verification as CI gates, not manual pre-release checklist items; a check that depends on someone remembering to run it will eventually not run.
Done when
1---2name: api-design-standards3description: API design discipline grounded in the Microsoft/Google REST guidelines, resource and URL conventions, a chosen versioning strategy, what counts as a backward-compatible vs breaking change, and consumer-driven contract testing. Use when designing a new API endpoint, changing an existing API's request/response shape, deciding how to version an API, or asking whether a change to a public interface is breaking.4---56# API Design Standards78An API is a promise to callers you don't control and usually can't see. Adding to that promise is free; changing or removing part of it breaks someone who already depended on it. Every API change is classified against that promise (additive or breaking) before it ships, not discovered after a caller files a bug.910## Phase 1: Resource and URL conventions1112- URLs name **resources** (nouns), never actions (verbs): `POST /orders`, not `POST /createOrder`. The HTTP method carries the verb.13- Collections are plural (`/orders`), a specific member is the collection plus an identifier (`/orders/{id}`), and nesting reflects real ownership (`/orders/{id}/items`), not an arbitrary path someone found convenient.14- HTTP methods map to a fixed meaning, and only that meaning: `GET` (read, no side effects, safe to retry and cache), `POST` (create, or a non-idempotent action), `PUT` (full replace, idempotent, calling it twice with the same body leaves the same state), `PATCH` (partial update), `DELETE` (remove, idempotent).15- Status codes communicate outcome precisely: `2xx` success (`201` for created with a `Location` header, `204` for success with no body), `4xx` the caller's fault (`400` malformed request, `401` unauthenticated, `403` authenticated but not authorized, `404` not found, `409` conflict, `422` semantically invalid), `5xx` the server's fault. Returning `200` with an error payload embedded in the body hides the failure from every generic HTTP-aware tool (retries, caches, monitoring).1617## Phase 2: Versioning strategy1819Pick one signal and use it consistently across the whole API; mixing strategies forces every client to handle more than one:2021| Strategy | How | Tradeoff |22|---|---|---|23| **URI path** (`/v2/orders`) | version segment in the path | most discoverable and cacheable, but encourages whole-API version bumps for a change that only touched one resource |24| **Header / content negotiation** (`Accept: application/vnd.api+json;version=2`) | version encoded in a request header | most correct REST semantics (a version is a different representation of the same resource, not a different resource) but far less discoverable: undebuggable by just reading a URL |25| **Query parameter** (`?version=2`) | version as a query param | simplest to add after the fact, easiest to accidentally omit and silently get a default |2627Version the API only when a breaking change (Phase 3) actually requires it. A version bump for a purely additive change forces every existing client to migrate for no reason; that cost is the same mistake a hand-picked SemVer bump is, applied to an API instead of a package.2829## Phase 3: What's backward-compatible vs breaking3031| Change | Compatible? |32|---|---|33| Adding a new optional request field | Compatible: old clients that don't send it are unaffected |34| Adding a new field to a response | Compatible **if** clients are expected to ignore unknown fields: state this expectation explicitly in the API's docs, since some client generators fail closed on unknown fields |35| Adding a new endpoint | Compatible |36| Removing or renaming a field, endpoint, or enum value | Breaking |37| Changing a field's type or meaning (a string that used to be an ID now being a display name) | Breaking |38| Making a previously-optional field required | Breaking |39| Changing default behavior for an omitted parameter | Breaking |40| Tightening validation on an existing field (was permissive, now rejects previously-valid values) | Breaking |4142When a breaking change is unavoidable (security fix, correcting a genuinely wrong design): communicate it ahead of the change, support the old and new shape in parallel for a stated deprecation window (see `dependency-upgrade-management` for the deprecation-signal mechanics), and only remove the old shape after that window closes.4344## Phase 4: Contract testing4546- An OpenAPI (or equivalent) schema is the API's published, machine-readable shape; validate requests and responses against it in CI so a gross schema violation is caught before it reaches a client.47- For APIs consumed by services you also control (internal microservices), add **consumer-driven contract testing**: the consumer writes a test capturing exactly what it expects from the provider, the provider verifies against that captured contract in its own CI. This catches a breaking change (a renamed field, a removed endpoint) at the point the provider would have shipped it, not after deploy, in the consumer's production logs.48- Run the schema validation and the contract verification as CI gates, not manual pre-release checklist items; a check that depends on someone remembering to run it will eventually not run.4950## Done when5152- [ ] Every endpoint uses resource-noun URLs and the HTTP method matching its actual semantics.53- [ ] The API uses one versioning signal consistently, and a version bump happened only because Phase 3 classified the change as breaking.54- [ ] Every changed field, endpoint, or default was checked against the Phase 3 table before shipping.55- [ ] Any unavoidable breaking change has a stated deprecation window with the old and new shape both live during it.56- [ ] Schema validation and (for internally-consumed APIs) contract tests run as CI gates.57- [ ] Generated prose uses commas, colons, periods, or reworded sentences instead of em dashes.