API Design¶
An API is a long-lived contract between independently changing components. Optimize it for clear semantics, safe evolution, predictable failure, and operability—not for mirroring internal tables or methods.
Design from Use Cases¶
Before choosing a protocol, define:
- consumers and their critical workflows;
- resources or operations and their invariants;
- expected request sizes, rates, and latency;
- authentication and authorization boundaries;
- retry, concurrency, and partial-failure behavior;
- compatibility and retirement policy.
Start with a few representative requests and responses. An API that makes the main workflow awkward has the wrong abstraction even if its schema looks tidy.
Choosing a Style¶
| Style | Good fit | Important cost |
|---|---|---|
| HTTP resource API | Public or broadly compatible request–response APIs | Client-specific aggregation can take several calls |
| GraphQL | Clients need different projections over a connected schema | Query cost, caching, and authorization complexity |
| gRPC | Typed internal calls and streaming in controlled environments | Browser and human-debugging friction |
| Asynchronous messages | Decoupled workflows and event distribution | Eventual results, duplicates, ordering, schema evolution |
Use one style well before combining several.
HTTP Resource APIs¶
Model stable domain resources with nouns:
GET /orders/42
POST /orders
PUT /orders/42/address
PATCH /orders/42
DELETE /orders/42
HTTP method semantics matter:
GETandHEADare safe and idempotent;PUTandDELETEare idempotent by definition of their intended effect;POSTandPATCHare not inherently idempotent;- a response code and headers remain meaningful to generic clients and intermediaries.
Idempotent does not mean “identical response” or “no logging”; it means repeating the same request has the same intended effect. For non-idempotent create or payment operations, accept a client-generated idempotency key and persist its result for a documented scope and duration.
Use status codes consistently:
200successful response;201resource created, usually withLocation;202accepted for asynchronous processing, with a way to inspect status;204success without a response body;400malformed request;401missing or invalid authentication;403authenticated but not permitted;404resource not found or intentionally concealed;409state conflict;412failed precondition;422syntactically valid content that cannot be processed as requested;429rate limit exceeded;5xxserver or upstream failure.
The current semantics are defined by RFC 9110.
Requests and Representations¶
- Use consistent field names, types, units, time zones, and nullability.
- Distinguish absent,
null, empty, and default values deliberately. - Represent timestamps in an unambiguous standard form and document precision.
- Treat identifiers as opaque strings at boundaries.
- Set media types correctly and reject unsupported types clearly.
- Put secrets in authorization mechanisms, not URLs or logs.
- Set explicit request and response size limits.
Do not expose internal database columns merely because serialization is easy. Public representations should remain stable as storage evolves.
Validation and Errors¶
Validate syntax, shape, ranges, relationships, permissions, and current state. Return all safe, actionable field errors when useful, but never expose stack traces, queries, secrets, or internal topology.
RFC 9457 defines application/problem+json:
{
"type": "https://api.example.com/problems/out-of-stock",
"title": "Item is out of stock",
"status": 409,
"detail": "Quantity 3 is unavailable",
"instance": "https://api.example.com/problems/occurrences/abc123",
"item_id": "sku-7"
}
Clients should branch on stable machine-readable fields such as type, not parse human-readable detail text. Include a request or trace identifier through a safe field or header for support correlation.
Pagination, Filtering, and Sorting¶
Offset pagination is easy and supports page numbers, but large offsets can be expensive and concurrent inserts can shift results. Cursor pagination is more stable and efficient for changing large collections.
{
"items": [{"id": "o_123"}],
"next_cursor": "opaque-token"
}
A cursor should be opaque to clients, encode or reference the complete ordering position, be integrity-protected when clients could tamper with it, and have documented expiry behavior.
Define an immutable tie-breaker for sorting, such as (created_at, id). Allowlist filter and sort fields; do not translate arbitrary client strings into SQL.
Concurrency¶
Prevent lost updates with conditional requests:
GET /documents/7
ETag: "revision-12"
PUT /documents/7
If-Match: "revision-12"
If the representation changed, return 412 Precondition Failed. Domain commands may instead use an explicit expected version. Document whether bulk operations are atomic, partially successful, or asynchronous.
Long-Running Operations¶
For work that cannot finish within the request deadline:
- accept the request and return
202; - return a stable operation URL or identifier;
- expose state such as pending, running, succeeded, failed, or cancelled;
- make submission idempotent where duplicates matter;
- define result retention and cancellation semantics.
Authentication and Authorization¶
Use established authentication protocols and validate authorization for the specific resource and action on every request. Tenant identity must come from trusted credentials or server-side mapping, not an unverified request field. Apply field-level controls when a representation mixes data with different sensitivity.
See Authentication and Security.
Rate Limiting and Overload¶
Define limits by relevant identity—tenant, user, credential, route, or cost—not only IP address. Return 429 for policy limits and 503 for temporary service inability when appropriate. Communicate retry timing when known. Expensive GraphQL or search requests may need cost-based limits.
For broader design considerations, see Rate Limiter.
Compatibility and Versioning¶
Prefer additive evolution:
- add optional fields and endpoints;
- tolerate unknown response fields;
- do not change the meaning or type of existing fields;
- provide defaults for newly optional request behavior;
- measure use before removing anything.
Breaking changes require an explicit migration path, overlap period, owner, telemetry, and retirement date. A version in the path, header, or media type does not replace this process. Avoid versioning until an incompatible contract genuinely needs it.
GraphQL Notes¶
- Design the schema around domain concepts, not storage tables.
- Enforce authentication, field-level authorization, depth or complexity limits, and bounded pagination.
- Batch or prefetch to avoid N+1 data access.
- Distinguish transport success from field errors and partial data.
- Evolve with additive fields and deprecation telemetry.
GraphQL reduces over-fetching for some clients; it does not remove backend cost or authorization work.
gRPC Notes¶
- Keep protocol buffer field numbers stable and never reuse removed numbers.
- Use deadlines and propagate cancellation.
- Design idempotency before enabling retries.
- Bound message sizes and streaming buffers.
- Map domain failures to stable status and structured details.
- Test backward and forward compatibility across deployed versions.
Documentation and Operations¶
Publish a machine-readable contract such as OpenAPI or Protocol Buffers plus examples of normal and failure flows. Document authentication, authorization, limits, idempotency, pagination, compatibility, and support contacts.
Observe request rate, latency, errors, payload sizes, rejected requests, consumer/version usage, and dependency failures. Do not log credentials or sensitive bodies by default.
Review Checklist¶
- Does the API fit its main workflows?
- Are method, status, retry, and idempotency semantics correct?
- Are authorization and tenant boundaries enforced server-side?
- Are payloads, query cost, pagination, and concurrency bounded?
- Are errors stable, actionable, and safe?
- Can the contract evolve without coordinated deployment?
- Is deprecation backed by telemetry and a migration plan?
- Can operators trace failures without exposing sensitive data?