API Design
Purpose
Design HTTP APIs that clients can use correctly without reading your source code, and that you can evolve without breaking them.
When to Use
- Designing a new API or a new endpoint on an existing one.
- Reviewing an API before it becomes public and therefore permanent.
- Introducing versioning, pagination, or idempotency to an API that lacks them.
- Writing or auditing an OpenAPI specification.
Capabilities
- Resource and URL modeling.
- Status-code semantics and consistent error payloads (RFC 9457 problem details).
- Pagination strategies and their trade-offs.
- Idempotency for unsafe methods.
- Versioning and deprecation without breaking clients.
- OpenAPI specification as the source of truth.
Inputs
- The domain operations the API must expose.
- Client types: first-party, third-party, mobile, machine.
- Expected volume, page sizes, and latency requirements.
Outputs
- An OpenAPI 3.1 specification.
- Consistent error and pagination formats across every endpoint.
- A documented versioning and deprecation policy.
Workflow
- Model resources, not procedures —
POST /orders/{id}/refunds rather than POST /refundOrder. When an operation genuinely is not a resource, it is fine to say so, but check first.
- Fix the status codes — 200 for a body, 201 with a
Location for creation, 202 for accepted-but-not-done, 400 for malformed, 401 versus 403 correctly, 409 for conflict, 422 for semantically invalid, 429 with Retry-After.
- Standardize errors — One shape, every endpoint. RFC 9457 problem details, with a machine-readable
type and field-level detail.
- Choose pagination deliberately — Cursor-based for anything that mutates or is large. Offset only for small, static datasets.
- Make unsafe methods idempotent — Accept an
Idempotency-Key header on POST; return the original response on replay.
- Version from day one — Even if the first version is the only one. Retrofitting a version is far worse than carrying one.
Best Practices
- Never return a bare array as a top-level response.
{"data": [...], "next_cursor": "..."} leaves room to add fields; [...] does not.
- 200 with
{"error": ...} is a defect. Clients branch on status codes.
- Field names are permanent. Choose them as carefully as a database column.
- Do not expose internal identifiers or enum integers. Use opaque IDs and string enums.
- Any list endpoint without a limit will eventually be called with a million rows behind it. Cap it, and document the cap.
- Deprecate with headers (
Deprecation, Sunset) and a timeline, not with an announcement nobody reads.
Examples
Cursor pagination and problem-details errors:
GET /v1/orders?limit=50&cursor=eyJpZCI6IjAxSFgifQ HTTP/1.1
200 OK
{
"data": [ { "id": "ord_01HX...", "total_cents": 4200, "currency": "USD" } ],
"next_cursor": "eyJpZCI6IjAxSFkifQ",
"has_more": true
}
POST /v1/orders HTTP/1.1
Idempotency-Key: 4f3a2b1c-...
422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/validation",
"title": "Order validation failed",
"status": 422,
"detail": "One or more line items are invalid.",
"errors": [
{ "field": "items[0].quantity", "code": "min", "message": "must be at least 1" }
]
}
Notes
- Cursor pagination is stable under concurrent inserts; offset pagination silently skips and duplicates rows. If the data mutates, offset is a correctness bug, not a performance one.
- Idempotency keys must be stored with the response and a TTL, keyed by (key, endpoint, request-hash). Returning a cached response for a different body under the same key is worse than not having idempotency at all.
- URL versioning (
/v1/) is the least elegant and the most operationally forgiving. Header-based versioning is cleaner and much harder to debug in a proxy log.
1---2name: api-design3description: Use when designing or reviewing an HTTP API. Covers resource modeling, status codes, pagination, idempotency, versioning, error formats, and the contract details that break clients when you get them wrong.4---56# API Design78## Purpose910Design HTTP APIs that clients can use correctly without reading your source code, and that you can evolve without breaking them.1112## When to Use1314- Designing a new API or a new endpoint on an existing one.15- Reviewing an API before it becomes public and therefore permanent.16- Introducing versioning, pagination, or idempotency to an API that lacks them.17- Writing or auditing an OpenAPI specification.1819## Capabilities2021- Resource and URL modeling.22- Status-code semantics and consistent error payloads (RFC 9457 problem details).23- Pagination strategies and their trade-offs.24- Idempotency for unsafe methods.25- Versioning and deprecation without breaking clients.26- OpenAPI specification as the source of truth.2728## Inputs2930- The domain operations the API must expose.31- Client types: first-party, third-party, mobile, machine.32- Expected volume, page sizes, and latency requirements.3334## Outputs3536- An OpenAPI 3.1 specification.37- Consistent error and pagination formats across every endpoint.38- A documented versioning and deprecation policy.3940## Workflow41421. **Model resources, not procedures** — `POST /orders/{id}/refunds` rather than `POST /refundOrder`. When an operation genuinely is not a resource, it is fine to say so, but check first.432. **Fix the status codes** — 200 for a body, 201 with a `Location` for creation, 202 for accepted-but-not-done, 400 for malformed, 401 versus 403 correctly, 409 for conflict, 422 for semantically invalid, 429 with `Retry-After`.443. **Standardize errors** — One shape, every endpoint. RFC 9457 problem details, with a machine-readable `type` and field-level detail.454. **Choose pagination deliberately** — Cursor-based for anything that mutates or is large. Offset only for small, static datasets.465. **Make unsafe methods idempotent** — Accept an `Idempotency-Key` header on POST; return the original response on replay.476. **Version from day one** — Even if the first version is the only one. Retrofitting a version is far worse than carrying one.4849## Best Practices5051- Never return a bare array as a top-level response. `{"data": [...], "next_cursor": "..."}` leaves room to add fields; `[...]` does not.52- 200 with `{"error": ...}` is a defect. Clients branch on status codes.53- Field names are permanent. Choose them as carefully as a database column.54- Do not expose internal identifiers or enum integers. Use opaque IDs and string enums.55- Any list endpoint without a limit will eventually be called with a million rows behind it. Cap it, and document the cap.56- Deprecate with headers (`Deprecation`, `Sunset`) and a timeline, not with an announcement nobody reads.5758## Examples5960**Cursor pagination and problem-details errors:**6162```http63GET /v1/orders?limit=50&cursor=eyJpZCI6IjAxSFgifQ HTTP/1.16465200 OK66{67 "data": [ { "id": "ord_01HX...", "total_cents": 4200, "currency": "USD" } ],68 "next_cursor": "eyJpZCI6IjAxSFkifQ",69 "has_more": true70}71```7273```http74POST /v1/orders HTTP/1.175Idempotency-Key: 4f3a2b1c-...7677422 Unprocessable Content78Content-Type: application/problem+json79{80 "type": "https://api.example.com/errors/validation",81 "title": "Order validation failed",82 "status": 422,83 "detail": "One or more line items are invalid.",84 "errors": [85 { "field": "items[0].quantity", "code": "min", "message": "must be at least 1" }86 ]87}88```8990## Notes9192- Cursor pagination is stable under concurrent inserts; offset pagination silently skips and duplicates rows. If the data mutates, offset is a correctness bug, not a performance one.93- Idempotency keys must be stored with the response and a TTL, keyed by (key, endpoint, request-hash). Returning a cached response for a *different* body under the same key is worse than not having idempotency at all.94- URL versioning (`/v1/`) is the least elegant and the most operationally forgiving. Header-based versioning is cleaner and much harder to debug in a proxy log.