--- name: awesome-api-design description: "Designs or reviews an HTTP API's shape before code: resources, versioning, pagination, idempotency, filtering. Use when asked to design an API or an endpoint." license: MIT metadata: author: Khasky tags: ["api", "design", "rest", "versioning", "pagination"] documentation: "https://github.com/khasky/awesome-agent-skills/tree/main/skills/awesome-api-design" --- # API Design Shape an HTTP API so its consumers can build against it for years without a breaking surprise. The contract is the product: URLs, methods, payloads, errors, and evolution rules are decided here, deliberately — everything left implicit becomes an accidental contract the first client depends on. ## When to Activate - "Design the API for X", "review this API design / OpenAPI spec", "how should we version / paginate / handle retries". - A design doc needs its API section made concrete (awesome-design-doc hands off here). - A new endpoint is being added to an existing API and must match its conventions. Do not activate to define the error envelope's fields or retry classification (awesome-error-standards owns that contract) or to audit implemented handlers for vulnerabilities (awesome-security-audit). ## Work Process 1. Inventory the incumbent conventions and the project's own words — an existing API's casing, id format, pagination style, and envelope win over any guideline here: consistency within one API beats global best practice. Only a new API starts from the defaults below. Read the recorded decisions (existing ADRs) and the project's glossary (`CONTEXT.md`, a domain doc, or the terms its code and tests already use) before naming a single resource: a path is a public, long-lived name, and one that renames a concept the codebase already has costs every reader a translation. 2. Model resources, not procedures — nouns with identity and lifecycle (`/orders/{id}`), actions as state transitions on them (`POST /orders/{id}/cancel` when a pure verb is unavoidable — never `/doCancelOrder`). Nest at most one level deep; deeper hierarchies become query filters (`/comments?post_id=…`), because every nesting level hardcodes an ownership assumption into every client URL. 3. Decide the evolution rules before v1 ships — additive changes (new optional field, new endpoint) go in place; anything breaking (remove/rename/retype a field, tighten validation) only ever creates a new version. Publish that taxonomy with the API so consumers know what is safe to ignore. Evolve by layering — a redesigned abstraction ships beside the old one and existing integrations keep working until their owners move; deprecation is announced with a sunset window, never enforced by an in-place change. 4. Design list endpoints for growth — opaque cursor pagination (encode the `(sort_key, id)` position, return `has_more` + `next_cursor` in a list envelope) over offset, which skips and duplicates rows under concurrent writes and dies on deep pages. Filtering and sorting are an allowlist of named parameters over indexed fields, never a pass-through to the query layer. 5. Make unsafe methods retry-safe — mutations accept an `Idempotency-Key`; same key + same body replays the stored response, same key + different body is 409. Without it, every client retry is a potential duplicate side effect. (Retry classification and the envelope format: awesome-error-standards.) 6. Specify concurrency and partial updates — updates that can conflict get optimistic concurrency (`ETag` + `If-Match`, 409 with the current version on mismatch); PATCH semantics are declared (merge-patch vs replace), and an empty PATCH is rejected, not silently a no-op. 7. Write the contract down as the source of truth — an OpenAPI/schema document that generates or validates the implementation, not prose that drifts from it. Ids are opaque and prefixed (`ord_…`), timestamps ISO 8601 UTC, money integer minor units with currency, enums closed with a documented default for unknown values on the consumer side. ## Design review checklist When reviewing an existing design, walk the same decisions as findings: - Naming and casing consistent across every endpoint; no mixed `camelCase`/`snake_case` payloads. - No verbs in resource paths except modeled state transitions; no RPC-style `/getX` endpoints beside REST resources. - Every list endpoint paginated from day one — retrofitting pagination is a breaking change. - Every mutation idempotent-by-key or documented as naturally idempotent. - Error responses reference one stable envelope (awesome-error-standards) — not per-endpoint improvisation. - Versioning and deprecation policy stated; no "we'll decide when we break something". - Webhooks the API sends are treated as public surface: signed, versioned, redeliverable — same contract discipline as endpoints. - Nothing in the response a client shouldn't see: internal ids, flags, or fields leaking through by serializer default. ## Change review When the input is a change to a shipped contract (a diff to a spec, schema, handler or payload), review the change, not the end state. Compatibility is never declared from the new schema alone: read the old and new shapes side by side, and the code on both sides of the wire. Walk four scenarios and record the result of each: - Old writer, new reader: a payload produced before the change reaches code after it. - New writer, old reader: a payload produced after the change reaches code that has not been updated yet (still-deployed servers, shipped mobile or SDK builds). - Rollback: the old code returns while values only the new code writes are already stored or in flight. - Payloads at rest: old-format messages in queues, cache entries, stored rows and files are read by new code, and new-format ones by old code after a rollback. Search for consumers both ways, in every repository you can reach: - A client reads a field the server stopped sending, renamed or retyped. This breaks silently: no error, a missing or wrong value. - The server returns a status code or error code the client has no branch for. - A request field becomes required, or a validation tightens, while existing callers omit it. - A new enum value reaches a consumer with an exhaustive switch or a closed type. "No consumer found" is not "unused": external clients, other repositories, scripts and stored data are invisible to a code search. Say what was searched and treat the rest as unknown. Verdict, one per change: **Compatible** (all four scenarios pass, evidence cited), **Order-dependent** (safe only in a stated deploy order or with a stated migration step; name it), **Breaking** (some scenario fails for a live consumer; the fix is a new version or a layered shape), **Not determinable** (a scenario or consumer set could not be checked; name what is missing and never round up to Compatible). ## Output ```text API Design — — Contract decisions: - — — ... Spec: Deviations from incumbent conventions: Open questions: ``` ## Self-check before delivering - Run the design review checklist above against your own output — a finding in your own design gets fixed before delivery, not shipped with a caveat. - Every contract decision names the consumer consequence it buys; "because best practice" is not a justification — delete or justify. - Step 1 has evidence: name the incumbent spec, routes, or client code actually inspected. Defaults applied to an API that has conventions is the failure mode. - Every mutation in the design answers the retry question (idempotency key or naturally idempotent — stated which); every list endpoint answers the growth question. - Each open question carries a recommended default; a bare question pushes the design work back to the reader. ## Anti-patterns | Anti-pattern | Instead | |---|---| | Offset pagination on a growing table | Opaque cursor over an indexed sort key | | Breaking change hidden as a "fix" | New version; the old shape never changes under a client | | Per-endpoint error shapes | One envelope, one shared client-side parser | | Sequential integer ids in URLs | Opaque prefixed ids; sequence leaks volume and invites enumeration | | `PUT` that silently drops unknown fields | Declared PATCH semantics; strict validation with named rejections | | Prose spec that trails the implementation | Machine-readable contract that generates or gates the code | | Designing for the first client's screen | Resources model the domain; view composition belongs to the client |