API design standards
Criteria verified as of August 2026. Re-verify on the web before committing to anything (§8).
1. Scope and triggers
Applies when designing, reviewing or evolving the contract of an API and its governance: openapi.yaml/openapi.json/swagger.json, .graphql/.graphqls files, .proto, .spectral.yaml/.spectral.js, redocly.yaml, buf.yaml/buf.gen.yaml, example collections and developer portals. Triggers: HTTP verbs and status codes, ETags and conditional requests, pagination, filtering, error format, Idempotency-Key, quota headers, versioning and Deprecation/Sunset, GraphQL schema, protobuf evolution, webhooks and payload signing, asynchronous operations, bulk endpoints, API gateway.
Guiding principle: the contract is the product and it is irrevocable in practice. A published endpoint has consumers you do not control; every design decision is taken knowing that withdrawing it will cost months of coexistence and communication (§7). Design the contract first, generate the code afterwards — never the other way round.
Not applicable: see microservices-architecture-standards (topology, cutting boundaries between services, AsyncAPI contracts and event design, sagas, distributed resilience), the language skills —python-standards, typescript-standards, go-standards, rust-standards, jvm-spring-standards, dotnet-standards, php-standards— (concrete framework implementation: routing, serialisation, DI), appsec-standards (threat modelling and finding triage), identity-access-management-standards (OAuth 2.1/OIDC flows, PKCE, token issuance and validation, authorisation engines), cicd-standards (the pipeline that runs the §4 gates), i18n-standards (the contract fixes the interchange format —ISO 8601 with zone, amounts in minor units with their ISO 4217 code, BCP 47 tags and negotiation via Accept-Language—; how that is presented to the user in their language and region is theirs. The rule that avoids the classic bug: an API does not return pre-formatted text nor dates without a zone), solidity-standards (a boundary worth naming: a contract's ABI is also a public contract, but with a difference that inverts this skill's criteria: it is immutable and it is not versioned. There is no /v2, no orderly deprecation and no migration window; what is deployed stays. The design of that interface and its evolution via proxy are theirs).
2. Default decisions
Verify the latest version on the web before pinning it in a real project (§8).
| Area | Default | Justifiable alternative |
|---|---|---|
| Style | REST over HTTP/JSON, contract-first design | GraphQL if the problem is multi-source aggregation for heterogeneous clients; gRPC for high-performance internal RPC |
| Contract format | OpenAPI 3.2.0 (stable since Sept 2025; migration from 3.1 without breakage) | 3.1 if critical tooling does not support 3.2 yet |
| OpenAPI 4.0 "Moonwalk" | Do not use: no release and no date; the OAI itself recommends 3.x | — |
| Contract linter | vacuum (Go, 100% compatible with Spectral rulesets, supports OAS 3.0/3.1/3.2) or Redocly CLI (@redocly/cli 2.x, ESM-only, Node ≥ 22.12) |
Spectral only in repos already built on it (§7: degraded maintenance, no 3.2 support) |
| Error format | RFC 9457 problem details (application/problem+json), obsoletes RFC 7807 |
— |
| Collection pagination | Cursor/keyset | Offset only in small, bounded catalogues with a stable order |
POST idempotency |
Idempotency-Key header (still an I-D, not an RFC) |
— |
| Quotas | RateLimit / RateLimit-Policy headers from the httpapi-ratelimit-headers I-D |
X-RateLimit-* only for compatibility with existing clients |
| Deprecation | RFC 9745 (Deprecation) + RFC 8594 (Sunset) |
— |
| Versioning | Major in the path (/v1), additive within the major |
Header/media-type versioning only with governance and tooling that support it |
| Webhook signing | HMAC-SHA256 over id.timestamp.payload (Standard Webhooks scheme) |
RFC 9421 (HTTP Message Signatures) if you need asymmetric signing or rotation without a shared secret |
| gRPC schema | proto3 | Editions (edition = "2024") only with a deliberate migration (§3.9) |
| Protobuf breaking changes | buf breaking in CI against the base branch |
— |
3. Structure and conventions
3.1 Resource modelling and HTTP semantics
- Resources = plural nouns, lowercase,
kebab-casein the path (/payment-methods/{id}), JSON fields with a single convention per API (snake_caseorcamelCase, chosen and linted). Never verbs in the path except for actions that are not resources (/orders/{id}/cancel) — and those, minimal and documented. - Maximum nesting two levels (
/orders/{id}/items); deeper than that, expose the subresource as a root with a filter. - Non-negotiable semantics:
GET/HEADsafe (they never mutate state, not even "just a counter");PUT,DELETEidempotent;PATCHnot idempotent unless explicitly designed to be;POSTneither safe nor idempotent → §3.5. PATCHwith a declared media type:application/merge-patch+json(merge semantics,nulldeletes) orapplication/json-patch+json(operations). A "hand-rolled PATCH" with no media type and no documentednullsemantics is forbidden.- Status codes with meaning, not decorative:
201+Locationon creation;202for asynchronous acceptance (§3.7);204with no body;400syntax/validation,401no valid credential,403valid credential without permission,404to hide existence when revealing it leaks information,409state conflict,412precondition failed,422invalid semantics,429quota,503+Retry-Afteron unavailability. Forbidden:200with{"error": ...}inside. - Errors with RFC 9457:
type(stable URI that resolves to documentation),title,status,detail,instance+ your own extensions (e.g.errors[]per field). Register thetypevalues in a versioned catalogue of the contract;detailis for humans,typefor machines. Never stack traces, internal paths or SQL in the error body. - Dates in RFC 3339/ISO 8601 with offset (UTC by default), money in integer minor units + ISO 4217, closed enumerations documented and extensible (clients must tolerate new values).
3.2 Conditional requests and caching
- Every individual-resource response carries an
ETag;PUT/PATCH/DELETEon it requireIf-Matchto avoid lost update: withoutIf-Match→428 Precondition Required(policy) or documented explicit acceptance; with a staleIf-Match→412. GETwithIf-None-Match→304with no body. Weak ETag (W/"…") if the representation varies in irrelevant details.- Explicit
Cache-Controlin every response (including private ones:no-storefor sensitive data) and correctVarywhen the response depends onAccept,Accept-Languageor authentication. An endpoint with no declared caching policy will end up cached by someone.
3.3 Pagination, filtering, sorting and sparse fieldsets
- Opaque cursor (
?limit=&cursor=) with response{ data: [...], next_cursor|links.next }. The cursor is opaque by contract: clients do not parse it and you can change its encoding. Always include alimitwith a server-enforced maximum (e.g. 100) and a documented default. - Offset (
?page=&per_page=) only with a small, bounded set and a stable order: it is O(n) in the database and produces duplicates/skips under concurrent writes. Keyset (?after_id=&after_created_at=) when you need a stable natural order without opacity. total_countoptional and on demand (?include_total=true): always computing it is the hidden cost that kills a large collection.- Filtering and sorting with an allowlist declared in the contract (
?status=active&sort=-created_at): no translating arbitrary parameters into the query (injection and DoS via a non-existent index). Maximum number of sort fields and of combinable filters, documented. - Sparse fieldsets (
?fields=id,name) to reduce payload; if the API is an aggregation tree with many shapes per client, that is the signal that the case belongs to GraphQL (§3.8), not to infinite parameters.
3.4 Versioning and deprecation
- Major version in the path (
/v1/…). Within a major, only additive changes: new optional fields, new endpoints, new values in extensible enums. Renaming, removing, changing a type, tightening validation or changing semantics is breaking even if the schema "compiles". - Clients are tolerant readers: they ignore unknown fields. Document it as a consumer requirement; it is what makes additive evolution viable.
- Retirement cycle: publish
vN+1→ announce →Deprecation(RFC 9745) in thevNresponses →Sunset(RFC 8594) with a date ≥ theDeprecationone →Linkwithrel="deprecation"/rel="sunset"to the migration guide → measure usage per consumer → retire. Minimum window published in writing (typical: 6-12 months in public APIs). - Retirement is decided with per-consumer telemetry, not with faith. Without usage metrics per version and per client, deprecation is impossible.
3.5 Idempotency and unsafe operations
POSTwith business effects (payments, orders, shipments) acceptsIdempotency-Key(UUID generated by the client). Contract: same key + same payload → same stored response; same key + different payload →422/409(do not execute); new key → execute. Declared retention TTL (24h typical) and later purge.- Deduplication is implemented with a uniqueness constraint in the database, not with a preceding
SELECT: real concurrency is the test case (§4). - Key state: store
in_progressso that two simultaneous requests with the same key do not execute twice (409to the second one, or wait).
3.6 Bulk and batch endpoints
- Only when there is evidence of N+1 in the client; not by default. Explicit semantics: either all-or-nothing (transactional, global
400) or partial with a207-equivalent detailing the per-element result with its own problem detail. Ambiguity here = guaranteed incident. - Hard limit on elements per batch, documented and validated. A large bulk ⇒ turn it into an asynchronous operation (§3.7).
3.7 Asynchronous and long-running operations
POST→202 Accepted+Locationto the operation resource +Retry-After.GET /operations/{id}returns{status: pending|running|succeeded|failed, result|error}with the error in RFC 9457 format.- The operation is a first-class resource with a stable id, timestamps and declared retention; it can be queried after it finishes. Explicit cancellation (
POST /operations/{id}/cancel) if the business needs it. - Completion notification by webhook (§3.10) in addition to polling; polling is the always-available fallback, never the only mechanism in long operations.
3.8 GraphQL
- Applies when the client needs to choose the shape of the data across many sources. It is not a "modern" alternative to REST: it trades the over-fetching problem for the cost of arbitrary queries.
- Schema: stable naming (
PascalCasetypes,camelCasefields), deliberate nullability (not everything nullable "just in case"), Relay connections pagination (edges/node/pageInfo), mutations with a single input type and a payload with typed domain errors (business errors are schema data, not entries inerrors[]). - N+1 must be solved with a dataloader (batch + per-request cache). A resolver that queries per element in a list is a performance bug, not a pending optimisation.
- Hard limits in production: maximum depth, complexity/cost per query with a per-client budget, alias and batching limits (array batching is an attack multiplier), execution timeout.
- Persisted operations / trusted documents as an allowlist in first-party clients: the client sends an id, the server only executes known documents. Distinguish them from APQ (Automatic Persisted Queries), which is bandwidth saving and is not a security control. For public APIs the allowlist is not viable → depth/cost limits + rate limiting are mandatory.
- Introspection disabled in production (defence in depth, not a barrier: assume the schema can be inferred).
GETonly for read queries and with CSRF protection;application/graphql-response+jsonas the response media type. - Evolution: GraphQL does not version; fields are deprecated (
@deprecated(reason:)) and retired with per-field usage telemetry.@defer/@streamare still outside the ratified spec: do not put them in the public contract (§8).
3.9 gRPC and protobuf
- Wire compatibility is the schema's responsibility: never reuse field numbers or change their type; use
reservedfor retired numbers and names. New fields always optional with a sensible default. - Enums: reserve value
0asUNSPECIFIED; adding values is additive, removing them is not. buf breakingagainst the base branch as a CI gate (§4) andbuf lintwith the standard ruleset. Schema registry (BSR or equivalent) if there are external consumers.- proto3 by default: Editions (
edition = "2023"/"2024") brings no new functionality and changes sensitive defaults (features.field_presencebecomesEXPLICIT), which turns a careless migration into a breaking change. Migrate only with a plan and verification (§8). - Errors:
google.rpc.Statuswith canonical codes; the mapping to HTTP is documented if there is a gRPC↔REST gateway.
3.10 Webhooks
- Payload signed with HMAC-SHA256 over
id.timestamp.payload(concatenated with.), headerswebhook-id,webhook-timestamp,webhook-signature. The receiver verifies over the raw bytes of the body, before deserialising. - Anti-replay: reject timestamps outside a tolerance window (300 s is the value recommended by Standard Webhooks and Stripe's default) and deduplicate by
webhook-idas an idempotency key. Both, not one. - Signature comparison in constant time. Support for multiple active secrets for rotation without downtime (sign with the new one, accept both during the window).
- At-least-once delivery with retries and exponential backoff + jitter, attempt limit, manual redelivery endpoint and visibility of the attempt history. The consumer replies
2xxfast and processes in the background; the sender's short timeout is documented. - Controlled egress: published list of outbound IPs or mTLS; the receiver validates that the destination is theirs. On the side that registers webhook URLs, validate against SSRF (no private/loopback/metadata IPs, no redirects to internal ranges).
3.11 HATEOAS, with judgement
- Full hypermedia (HAL, JSON:API, Siren) only pays off when there are generic clients or flows with state-dependent transitions. In APIs consumed by first-party clients it generates cost with no return.
- Practical rule: include
linksfor pagination, for related resources and for actions available depending on state (cancelable), and nothing else. Do not invent a hypermedia engine nobody is going to use.
4. Quality and CI gates
In increasing order of cost; all of them block the merge (main always green):
- Structural validation of the contract (
redocly lint/vacuum lint/buf lint): breaks if the document is not valid for the declared version. - Your own style ruleset versioned in the repo (naming, plural, unique
operationId, mandatorydescription, examples,4xx/5xxdeclared withapplication/problem+json,securitypresent in every operation). The ruleset is what turns "good practices" into a gate. - Compatibility diff against the published version:
redocly/oasdifffor OpenAPI,buf breakingfor protobuf, GraphQL schema check (graphql-inspectoror equivalent). A breaking change without a major bump breaks the build. - Contract tests: the server is validated against its own contract (request/response validation in integration tests) and the key consumers against verified doubles. Generating client and server from the same contract proves nothing: validate real responses.
- Mandatory edge tests, not just the happy path: pagination on the last element and with an invalid/expired cursor; stale
If-Match→412; concurrently repeatedIdempotency-Key(two simultaneous requests, not sequential); payload at the limit and above the limit; unknown enum;429with correct quota headers; webhook with an invalid signature, an expired timestamp and a duplicated id. - Contract examples validated against their schemas (an example that does not validate is false documentation) and documentation generated in CI.
5. Security
- Reference: OWASP API Security Top 10 — 2023 edition (still the current one as of Aug 2026; there is no 2026 edition despite what third-party blogs announce). Real priority: BOLA/BOPLA (object-level and property-level authorisation) and API inventory.
- Object-level authorisation on every endpoint: the id existing does not mean it belongs to the caller. Forbidden to rely on unguessable ids as an access control. Property-level authorisation too: do not serialise the whole model (
role,internal_notes) nor accept mass binding — allowlist of input and output fields. - Authentication: OAuth 2.1 / OIDC with short-lived tokens for user access and
client_credentialsfor machine-to-machine; verifyiss,aud,expand the signature in every service. API keys only for application identification and quota, never as the sole authentication for sensitive operations; if they exist, with an identifiable prefix, hashed at rest, scoped and rotated. Forbidden: long-lived tokens without rotation, secrets in the query string,Basicoutside an internal channel with mTLS. - Declare
securityper operation in the contract, not only globally: an endpoint that forgot to inherit it is an open endpoint. - Strict validation at the boundary against the contract schema: types, formats, lengths, ranges,
additionalProperties: falsewhere applicable, maximum body size and JSON depth. Reject what does not fit; do not "sanitise" by guessing. - Rate limiting per identity and per operation (not just per IP), with differentiated cost for expensive endpoints;
429+Retry-After+ quota headers. Global concurrency limit and page-size limit as protection against application-level DoS. - Never leak existence, internal structure, versions or traces in the error: the RFC 9457
detailis written by you, not by the framework. Correlate with atrace_idin the response for support. - Restrictive CORS: explicit origin, never reflecting the
OriginwithAccess-Control-Allow-Credentials: true. - Inventory: every deployed API is in the catalogue with an owner, version and status. Shadow APIs and the versions "nobody uses any more" but that still respond are the recurring audit finding.
6. Performance and operability
- Latency budget per endpoint and SLO published in the portal (p95/p99 + availability). Without an SLO, the API has no operational contract.
- Metrics per operation (
operationId, not per URL with ids): latency, error rate by code, quota usage, and usage per version and per consumer (indispensable for §3.4). - Negotiated compression, known
Content-Length, streaming (SSE or chunked) for large responses — OpenAPI 3.2 already describes streaming natively. - API gateway for cross-cutting concerns: TLS 1.2+/1.3, authn, rate limiting, validation against the contract, observability. Forbidden to put business logic or domain-specific payload transformations in the gateway.
- Developer portal generated from the contract (never written by hand in parallel): reference, authentication guide, changelog per version, catalogue of error
types, test environment and deprecation policy. Documentation that diverges from the contract = false documentation. - Sandbox/mock server generated from the contract so consumers can integrate before the implementation exists.
7. Sustainability and governance
- Design-first: the contract is reviewed in a PR before implementing, with an API reviewer other than the author in public APIs. Contract review is a human gate, not a formality.
- Style ruleset shared across the organisation's APIs, versioned and with a process for changing it. Consistency between APIs is a product attribute.
- ADR for one-way decisions: style (REST/GraphQL/gRPC), versioning scheme, error format, authentication model, deprecation policy.
- Written and published breaking-change policy: what counts as breaking, minimum coexistence window, notification channel, support commitment per major version. In public APIs it is a contractual commitment, not an intention.
- Cadence: quarterly review of the inventory (live versions, usage per consumer, retirement candidates) and of the tooling versions (§8).
FORBIDDEN
- ❌ Breaking change within a major version (renaming/removing fields, changing type or semantics, tightening input validation).
- ❌
200 OKwith an error in the body; errors without RFC 9457;detailwith a stack trace, SQL or internal paths. - ❌
GETthat mutates state;POSTwith effects and noIdempotency-Keysupport. - ❌ Offset pagination in collections that grow; a collection with no server-enforced maximum
limit. - ❌ Filters or sorting built from arbitrary parameters without an allowlist.
- ❌ Authorisation based on unguessable ids; serialising the full internal model; mass binding of the input.
- ❌ An endpoint with no
securitydeclared in the contract, or with no object-level authorisation. - ❌ Deprecating without
Deprecation/Sunset, without a published deadline and without per-consumer usage telemetry. - ❌ Retiring a version before the communicated deadline — or leaving it alive indefinitely "just in case".
- ❌ Documentation written by hand in parallel to the contract; examples that do not validate against their schema.
- ❌ GraphQL in production without depth/complexity limits, without a dataloader or with introspection open.
- ❌ Confusing APQ with an operation allowlist and calling it a security control.
- ❌ Reusing protobuf field numbers or changing their type; publishing
.protowithoutbuf breakingin CI. - ❌ Webhooks without a signature, without a timestamp tolerance window or without deduplication by id.
- ❌ Accepting webhook URLs without anti-SSRF validation.
- ❌ Adopting OpenAPI 4.0 "Moonwalk" in a real project (there is no release).
- ❌ Business logic in the API gateway.
8. Mandatory web verification
Before committing any fact from this document to a deliverable, look it up — do not recall it:
- OpenAPI: current stable version (3.2.0 since Sept 2025) and the real status of 4.0/Moonwalk in
github.com/OAI/sig-moonwalkandopenapis.org— as of Aug 2026 it still has no date and the OAI itself recommends 3.x. - RFCs and drafts in
datatracker.ietf.orgbefore citing them: RFC 9457 (problem details, Jul 2023, obsoletes 7807) ✔; RFC 9745 (Deprecation) ✔; RFC 8594 (Sunset, informational) ✔; RFC 9110 (HTTP Semantics) ✔; RFC 9651 (Structured Fields) ✔. Careful: RFC 9331 is not rate limiting, it is ECN/L4S — the quota headers are still indraft-ietf-httpapi-ratelimit-headers(rev. -11, May 2026, expires Nov 2026) and their syntax has changed several times (todayRateLimit-Policy: "sliding";q=12;w=1/RateLimit: "sliding";q=12;r=1;t=1): verify the current revision before implementing it.Idempotency-Keyis still an I-D, not an RFC. - Linters: latest version of Redocly CLI (
@redocly/cli, 2.x, ESM-only, Node ≥ 22.12) and vacuum, and their OAS 3.2 support. Maintenance status of Spectral (heavily degraded activity in 2025-2026, no 3.2 support; a community fork exists) before choosing it for a new project. - GraphQL: current ratified edition (September2025 at
spec.graphql.org) and the status of@defer/@streamand incremental delivery — as of Aug 2026 they were still pending in the spec despite being in graphql-js v17+. Status of the standardisation of persisted documents in GraphQL-over-HTTP. - Protobuf/gRPC: Buf CLI version and its current guidance on Editions vs proto3 (
buf.build/docs,protobuf.dev/editions) — the conservative recommendation cited here is from 2024. - Webhooks: current revision of the Standard Webhooks spec (
standardwebhooks.com) and of RFC 9421 before pinning headers or algorithm. - OWASP API Security Top 10: current official edition at
owasp.org/API-Security— as of Aug 2026 it is the 2023 one; articles titled "2026" repackage that list. - CVEs and EOL of any gateway, GraphQL server or library you recommend (
endoflife.date, project advisories).
If the web contradicts this document, the web wins — flag the discrepancy.