Skip to content

HTTP and API design

Status: Complete. Last reviewed 2026-08-28.

An API is a compatibility promise about capabilities, representations, errors, and change—not a reflection of controller methods or database tables. Good design makes retries, authorization, partial failure, and evolution explicit enough that independently deployed clients can behave safely.

Start from the domain action and ownership boundary. Resource-oriented APIs expose stable identities and representations; command endpoints are appropriate when an operation does not naturally mean CRUD, has important workflow semantics, or produces an asynchronous result. /payments/{id}/captures can be clearer than pretending every action is a generic update.

URLs should be stable and opaque to clients beyond documented structure. Use HTTP methods according to their semantics. GET retrieves without requesting state change. PUT replaces the state of a target resource and is idempotent. PATCH applies a documented partial-update format and is idempotent only if that format and operation make it so. POST creates subordinate resources or invokes commands and needs application-level replay protection when repeats are unsafe.

Choose status codes from the protocol meaning, then define application detail. A successful asynchronous acceptance can return 202 with a status resource. Creation can return 201 and Location. A conditional update conflict may be 409, while a failed precondition tied to an HTTP conditional can be 412. Do not encode every outcome as 200 with an error flag; clients, caches, monitors, and intermediaries lose useful semantics.

Keep boundary responsibilities distinct:

  1. Parse media type, syntax, and shape into an application-owned input.
  2. Authenticate the caller and authorize the capability against the target scope.
  3. Enforce domain invariants in the transaction or consistency boundary that owns them.
  4. Translate outcomes into a documented status, fields, and headers.
  5. Serialize a representation intentionally rather than leaking ORM objects.

Validation proves that input is structurally acceptable; it does not prove the actor may use a referenced identifier or that a concurrent invariant remains true. Avoid mass-assigning transport payloads directly into persistence. Output DTOs/resources prevent accidental exposure, hidden lazy-loading queries, and breaking changes when storage evolves.

Media types and field presence are part of the contract. Distinguish missing, null, empty, and default. For partial updates, define whether null clears a value and whether arrays replace or merge. JSON Patch and JSON Merge Patch have different semantics; a custom “PATCH-shaped JSON” must be documented just as precisely.

An error representation should provide a stable machine-readable type/code, human-readable summary, correlation identifier, and structured field violations where appropriate. RFC 9457 problem details offers common fields and extension points. Do not make clients parse English messages, and do not expose stack traces, SQL, secrets, internal hosts, or raw vendor bodies.

Separate categories that drive client behavior: malformed request, unauthenticated, unauthorized, missing/not-visible resource, invariant conflict, throttled request, and transient/unavailable dependency. Document retryability and include Retry-After when its semantics apply. A correlation ID supports diagnosis but is not a secret or an authorization token.

Authorization denial can intentionally resemble not-found when revealing existence is unsafe, but use a consistent policy. Batch APIs need per-item outcomes or an atomic all-or-nothing contract; a single top-level success cannot express partial effects safely.

Compatibility is consumer-specific. Adding an optional response field is usually safe for clients that ignore unknown fields, but it can break strict decoders, signatures, snapshots, or generated types. Adding an enum value can break exhaustive switches. Making an optional request field required, changing nullability, narrowing accepted input, reinterpreting a field, or changing ordering is generally breaking.

Prefer additive evolution, tolerant response readers, explicit defaults, and expand/migrate/contract rollout. Measure actual client versions before removal. Deprecation needs an alternative, announcement, observability, and sunset policy; a version number alone does not coordinate migration.

Choose a versioning mechanism—path, media type, field/capability negotiation, or date/account version—based on routing and client needs. Avoid new versions for every additive change, but do not hide incompatible semantics behind the same contract. Internally deployed clients still need compatibility during rolling deployments.

OpenAPI can describe paths, schemas, security requirements, and examples and can drive validation or clients. It cannot decide ambiguous business semantics, guarantee implementation behavior, or replace consumer tests. Treat the published document as reviewed source and detect drift in CI.

Offset pagination is simple and supports page numbers, but large offsets may require scanning/skipping work and concurrent inserts/deletes shift later pages. Keyset/cursor pagination continues after a stable ordered key and is generally efficient for traversal. Its ordering must be deterministic and indexed, commonly (created_at, id) rather than a non-unique timestamp alone.

Make cursors opaque, signed if tampering matters, and bound to filters, sort order, tenant, and direction. Define whether the result is a live traversal or snapshot; ordinary keyset pagination avoids many duplicates/shifts but does not provide a historical snapshot when rows change. Cursor expiry and deleted boundary rows need specified behavior.

Return a next link/cursor and a clear limit. Total counts can be expensive or inconsistent with live results, so provide them only when the product needs them and define whether they are exact. Apply maximum page sizes and stable tie-breakers to prevent resource abuse and missing records.

An idempotency key identifies one logical operation, not merely one HTTP attempt. Scope it by caller and operation, validate that a reused key has the same request fingerprint, and persist the key with the durable operation/result. Concurrent requests with the same key must converge; a check-then-insert cache entry can itself race.

Define retention and what response is replayed. If the server times out after an external provider succeeds, a retry should resolve the same provider operation ID rather than create another charge. Idempotency does not mean success: the same key can replay a deterministic failure according to policy.

For lost-update prevention, expose a version/validator and require If-Match or an application version field. A stale writer receives a precondition/conflict response and must reload or merge. This protects optimistic concurrency; it is different from deduplicating repeated commands.

Webhooks are authenticated, replayed messages

Section titled “Webhooks are authenticated, replayed messages”

A webhook sender can retry, deliver duplicates, arrive out of order, and time out before seeing acknowledgement. Verify authenticity over the exact raw body and signed metadata before parsing/mutating. Use the provider’s current signature scheme, constant-time comparison where applicable, a narrow timestamp tolerance, secret rotation with overlap, and HTTPS. An IP allowlist can be defense in depth but is rarely sufficient identity.

Persist a stable provider event ID under a unique constraint and make processing idempotent. Return quickly after durable receipt when the provider’s contract permits, then process asynchronously. Do not mark an event complete before its effects commit. If events for one entity require order, carry and enforce a version or fetch current provider state; arrival order is not authority.

Protect the endpoint with body limits and rate controls, retain enough evidence for audit without logging secrets, and provide replay/reconciliation tooling. A valid signature authenticates the sender/message, not the business validity of every referenced state transition.

Define request and response size limits, timeouts, quotas, rate-limit identity, and overload behavior. Rate limits should return documented fields and avoid leaking other tenants’ activity. Long operations should become status resources or durable jobs rather than holding connections indefinitely.

Observe per-operation latency, status/error code, client version, payload size, and dependency outcome without high-cardinality or sensitive labels. Distributed traces and correlation IDs help locate failures, while consumer-driven or schema compatibility tests catch drift. Redact credentials, tokens, personal data, and signed webhook content.

  • Current: HTTP semantics use RFC 9110; problem details is RFC 9457, which obsoletes RFC 7807.
  • Common: REST-style JSON with OpenAPI, cursor pagination, idempotency keys, and signed webhooks coexist with RPC/command endpoints.
  • Legacy: Undocumented 200-with-error envelopes, offset-only pagination, ORM serialization, and permanent “v1” contracts should be migrated with consumer evidence, not abruptly renamed.