Design an HTTP API
Activated Cloud✓ Officialactivated/design-http-api
Free · MIT
About
Designs or changes an HTTP API so clients can rely on it: resource-oriented URLs, correct methods and status codes, consistent naming, structured problem-details errors, token-based pagination, idempotency keys for safe retries, optimistic concurrency, authorisation on every object, versioning and a strict rule for what counts as breaking, all written into an OpenAPI description with contract tests. Use when adding or changing endpoints, reviewing an API design or planning a new version. Not for debugging calls to an existing API (use debug-root-cause).
Documentation
Design an HTTP API
An API is a contract that outlives the code behind it: once a client depends on a field, a status code or an error shape, changing it breaks someone you may never meet. Good design is consistent (one way of doing each thing), predictable (standard HTTP semantics), safe to retry, and explicit about what may change. Match the existing API's conventions first; consistency beats any guideline.
When to use
- "Add an endpoint for X", "design the API for this feature", "review this API spec", "we need a v2".
- Changing a request or response shape, a status code or an error format.
- Exposing an internal service to partners or the public.
What you need
- The use cases: who calls this, what they need to do, how often, from where (browser, mobile app, server).
- The existing API's conventions: naming case, error format, pagination style, auth, versioning. Read two or three existing endpoints and any OpenAPI file (
search_filesforopenapi,swagger). - Consumers of anything you change: search the repo and other repos you can reach for the path and field names; ask the owner about external clients.
- The conventions reference:
references/api-conventions.md.
Method
Model resources, not actions. Name the nouns (orders, invoices, members) and their relationships. URLs identify resources:
/orders,/orders/{order_id},/orders/{order_id}/refunds. Use plural nouns, lowercase, and one path-segment case consistently (kebab-case is common:/payment-methods). Keep nesting to one level of ownership. For operations that are not CRUD (cancel, resend), either model the result as a resource (POST /orders/{id}/cancellation) or use a clearly named action segment the codebase already uses (POST /orders/{id}:cancelor/orders/{id}/cancel); pick one style per API.Use methods for what they mean.
Method Use Safe Idempotent GET Read yes yes POST Create, or trigger a non-idempotent action no no (make it idempotent with a key, step 6) PUT Replace a whole resource at a known URL no yes PATCH Partial update no not by default DELETE Remove no yes (a repeat returns 204 or 404, consistently) GET never changes state. Do not tunnel writes through GET or reads through POST without a reason (very large queries are the usual exception). Return precise status codes. 200 with a body, 201 with a
Locationheader for creation, 202 for accepted asynchronous work (with a way to poll it), 204 with no body. Client errors: 400 malformed, 401 not authenticated, 403 authenticated but not allowed, 404 not found (also for objects the caller may not know exist), 409 conflict with current state, 412 precondition failed (stale ETag), 415 wrong content type, 422 well-formed but invalid, 429 rate limited withRetry-After. Server errors: 500, 502, 503 withRetry-After, 504. Never return 200 with an error inside the body.One error shape everywhere. Use problem details (RFC 9457, media type
application/problem+json) unless the API already has a format:type,title,status,detail,instance, plus anerrorslist for field validation. Never put stack traces, SQL or internal hostnames in responses; log them server-side with a request ID and return the ID.Paginate every list from day one. Prefer opaque page tokens: the client sends
page_sizeandpage_token, the response returnsitemsandnext_page_token(absent on the last page). Tokens survive inserts and deletes and scale to large tables; offset pagination (offset,limit) skips or repeats rows when data shifts and gets slow deep in a table, so keep it for small, stable lists. Cappage_sizeand document the default and the maximum. Filtering and sorting through documented query parameters (status=paid,sort=-created_at).Make retries safe. Networks fail after the server has acted, so clients retry. For POSTs that create things or move money, accept an
Idempotency-Keyheader: store the key with the result, and return the same result for a repeat with the same key (and an error if the same key arrives with a different body). Keep keys for a documented period (often 24 hours).Prevent lost updates. Return an
ETag(or a version field) on reads; requireIf-Matchon updates to resources that several actors edit; answer 412 when it is stale.Name and format data consistently. One case for JSON fields across the API (match what exists; snake_case and camelCase are both common). Timestamps in RFC 3339 with time zone (
2026-10-05T14:10:00Z). Money as an integer in minor units plus a currency code, or a decimal string, never a float. IDs as strings (they may not stay numeric). Enums as lowercase strings, documented, and clients told to tolerate unknown values. Booleans named as questions (is_active,has_payment_method).Secure every endpoint. Authenticate every request unless deliberately public. Authorise on the object, not just the route: check that this caller may read or change this specific order (the most common API vulnerability is fetching another customer's object by changing an ID). Validate input at the boundary (types, lengths, ranges, allowed values). Rate limit, especially auth and expensive endpoints. Never put secrets or personal data in URLs (they end up in logs). Set CORS narrowly for browser clients.
Decide compatibility before you ship. The breaking-change list in
references/api-conventions.mdis the rule. Adding optional request fields, new response fields, new endpoints and new enum values (when clients were told to tolerate them) is compatible. Removing or renaming anything, changing types, formats or defaults, adding required inputs, tightening validation and changing status codes or error shapes is breaking. Breaking changes need a new version (in the path, a header or a media type: follow the API's existing scheme) and a deprecation period for the old one, announced with dates. Ask the owner before shipping any breaking change.Write it down as OpenAPI. Update or create the OpenAPI description with every path, parameter, schema, example and error response. Lint it if the project has a linter (Spectral is a common open-source one:
npx @stoplight/spectral-cli lint openapi.yaml). Generate or check docs from it.Test the contract. Tests for each endpoint: happy path with exact status and body shape; each documented error (400, 401, 403, 404, 409, 422); authorisation (another user's ID returns 404 or 403); pagination across page boundaries; idempotent retry returns the same result. Where the project supports it, validate responses against the OpenAPI schema in tests.
Review with a consumer's eyes. Before building, show the design (endpoints, example requests and responses, errors) to the owner or the client team (
show_cardor a short doc). Changing a design is cheap; changing a shipped API is not.
Output
The endpoint design (method, path, request, response, errors, auth, pagination and idempotency notes) in the OpenAPI file plus a short summary for review, then the implementation and contract tests if asked to build it. For changes to existing endpoints: a compatibility verdict (compatible or breaking, with the reason) and the migration and deprecation plan when breaking. A design summary for review looks like this:
POST /orders/{order_id}/refunds
Auth: staff token with `refunds:write`; caller must belong to the order's merchant (else 404).
Headers: Idempotency-Key required (24 h retention).
Body: { "amount": 1500, "currency": "EUR", "reason": "damaged" } amount in minor units, at most captured minus refunded
201: refund object + Location: /orders/{order_id}/refunds/{refund_id}
Errors (problem+json): 404 order not found, 409 order not captured, 422 amount too high or currency mismatch, 429 rate limited
Compatibility: new endpoint, no change to existing ones.
Tests: happy path, each error, another merchant's order returns 404, repeat with same key returns the same refund.
Checks before you finish
- Methods, status codes and the error shape follow the rules above and match the existing API.
- Every list endpoint is paginated with a capped page size.
- Every endpoint authenticates, authorises on the specific object, and validates input.
- The change is classified as compatible or breaking against the list, and breaking changes have the owner's approval and a versioning plan.
- The OpenAPI description and contract tests are updated and pass.
Pitfalls
- Inventing a new style per endpoint. Inconsistency is a tax on every client. Copy the existing conventions.
- Unpaginated lists. They work in development and fall over at the first big customer.
- 200 with
{"error": ...}. Clients, proxies and monitoring all trust the status code. - Leaking internals in errors. Stack traces and SQL in responses help attackers, not users.
- Floats for money, local times without zones. Both corrupt data quietly.
- "Small" breaking changes. Renaming a field or tightening validation breaks someone. Classify every change.
- Checking the route but not the object. Logged-in users fetching other customers' records by ID is the classic API breach.
Versions
Listed from the source repository.
Reviews
No reviews yet. Be the first.
