API Design Patterns & Best Practices
Interactive API design tool: calculate quality scores, analyze performance metrics, and optimize API architecture.
What is API design?
API design is the work of defining a stable, testable contract between a client and a service. The contract names resources and operations, describes request and response schemas, identifies the caller, limits work, classifies errors, explains retry behavior, and states how the interface can evolve.
In plain language, an API lets two programs cooperate without knowing each other's implementation. A client should be able to predict what one request means, which result proves success, how to recover from failure, and whether tomorrow's service release will preserve that behavior.
The core invariant is one documented client intent has one predictable meaning across success, rejection, timeout, retry, and version change. A route that works in the happy path but can double-charge after a lost response is not a complete contract.
Review API Design Principles first if HTTP resources, methods, and status classes are unfamiliar.
Meaning
Name the intent
Resources, methods, schemas, and preconditions define what the request means.
Bounds
Limit the work
Page size, payload, deadline, rate, and concurrency limits protect both parties.
Recovery
Resolve uncertainty
Errors, operation state, idempotency, and reconciliation tell clients what to do next.
Evidence
Prove the promise
Specifications, contract tests, traces, and SLOs keep implementation aligned.
Design from the client workflow inward
Start with the business outcome the client needs, not the controller method or database table already in the server. A useful contract exposes domain meaning while leaving storage, queues, and service boundaries free to change.
1 Outcome
Name the client intent
State the result in client language: read one order, search orders, create one payment, or start one export.
2 Meaning
Choose the resource boundary
Give the durable noun a stable identity and use the method, path, and preconditions to express the allowed transition.
3 Capacity
Bound request work
Cap payloads, page size, fan-out, processing time, retries, and concurrency before the route reaches production scale.
4 Recovery
Specify every outcome
Define success, validation, authorization, conflict, limit, timeout, and dependency results with a safe next action.
Keep three boundaries separate
What exists?
Representation
A resource schema describes durable state that clients may read. Stable IDs, field meaning, formats, and optionality matter more than mirroring internal columns.
What may change?
Transition
An operation changes or creates state under explicit preconditions. The method and result must reveal whether work completed now or only started.
Who may do it?
Policy
Authentication resolves identity; authorization checks this caller, resource, tenant, scope, and business state. A valid schema does not grant permission.
Lab: turn a workflow into a client contract
Choose a workflow, interface shape, completion promise, and collection policy. Watch the generated endpoint, client follow-up, response bound, and design findings change together. The goal is not a universal score; it is a contract whose consequences match the workload.
Route every request through explicit owners
An API is a boundary, not the whole system. The gateway can authenticate and apply coarse abuse controls, but the owning service must still authorize the resource, enforce business invariants, create the durable effect, and classify the result.
A bounded API request
Each stage owns one decision and passes a deadline, identity, and correlation ID forward.
Intent
Client
Sends a documented request, stable idempotency identity where required, and a deadline. It handles only errors and retries the contract declares recoverable.
Admission
Edge and gateway
Authenticate, validate content type and coarse size, apply rate policy, assign a request ID, and reject work that has no remaining deadline.
Meaning
Owning service
Authorize the resource and operation, validate business state, bind idempotency, and coordinate the authoritative transition.
Durability
Data and dependencies
Commit state under bounded calls and explicit consistency rules. A dependency timeout becomes a known service outcome, not a leaked exception.
Evidence
Response and telemetry
Return a stable representation or error envelope and record route, outcome, latency, version, and trace context without sensitive labels.
Give HTTP semantics one clear meaning
Use lowercase plural resource nouns and stable identifiers. Let standard methods carry their usual semantics so clients, caches, gateways, SDKs, and operators can reason about the route without private knowledge.
Safe operation
Read
GET /v1/orders/{orderId} returns a representation without changing business state. Validators such as ETags can make repeat reads efficient.
New resource
Create
POST /v1/orders creates under a collection. Return 201 Created, the resource location, and an idempotency policy when retries could repeat the effect.
Existing resource
Replace or patch
Use PUT for documented replacement and PATCH for a declared patch format. Require a version precondition where concurrent updates could overwrite work.
Lifecycle transition
Delete
State whether repeated deletion returns the same terminal outcome, whether deletion is asynchronous, and how clients observe retention or restoration windows.
Domain action
Command
Use an action endpoint only when a meaningful transition does not fit resource creation or update. Name it in domain language and expose its resulting state.
Tracked operation
Long-running work
Return 202 Accepted only with a durable operation ID, status URL, terminal states, expiry policy, and cancellation behavior. Acceptance is not completion.
Make collection work finite
- Cap page size and response bytes; reject requests that ask the server to scan or return an unbounded collection.
- Prefer opaque cursors for large mutable collections. The cursor should bind ordering and continuation state without exposing storage internals.
- Define a stable sort with a unique tie-breaker so concurrent inserts do not make rows disappear or repeat between pages.
- Allowlist filters and operators. An arbitrary expression language can turn one valid request into an expensive full scan.
- State freshness and consistency. A fast list from a replica may lag a write that the client just completed.
Capacity is part of the contract. At 2,000 requests per second, a 100-item page can serialize 200,000 items per second. A 64 KB response ceiling implies about 128 MB/s before protocol overhead and replication, so page, field, and compression choices need measurement.
Return errors that programs can act on
Use one machine-readable envelope with a stable error code, safe human message, request or trace ID, and optional field details. Do not expose stack traces, SQL, tokens, or secrets. Clients should branch on the stable code and status, not parse prose.
Separate the failure classes
- Invalid request: malformed syntax, unsupported content type, invalid field, or failed business validation. Point to the relevant field without echoing sensitive data.
- Unauthenticated or forbidden: distinguish missing or invalid identity from a caller that lacks permission for this resource and action.
- Conflict or stale state: identify version, uniqueness, or lifecycle conflicts and explain whether the client should refresh, choose again, or stop.
- Rate or capacity limit: return the policy scope and safe retry guidance. Do not invite every caller to retry at the same instant.
- Server or dependency failure: return a stable service code and correlation ID. Document whether a read may retry and how a write must reconcile.
Status codes provide a shared class, while application error codes carry stable domain meaning. For example, 409 Conflict may contain order_version_stale or idempotency_key_reused so the client knows which recovery path applies.
Publish one executable source of contract truth
An OpenAPI document can describe operations, parameters, schemas, security, examples, and reusable errors. Generate it from the implementation or validate both against the same schema in CI. A manually maintained document that drifts from runtime behavior creates a second, false API.
Test the boundary at several depths
- Unit-test serializers, field validation, authorization decisions, idempotency fingerprints, and domain transitions.
- Integration-test handlers with real databases and faithful dependency behavior, including deadlines and transaction failures.
- Run provider and consumer contract tests against every supported client shape.
- Diff the OpenAPI contract for removed fields, newly required inputs, enum changes, status changes, and altered retry semantics.
- Inject lost responses, duplicate deliveries, stale preconditions, and dependency saturation before release.
Make non-idempotent writes recoverable
Network delivery is at-least-once unless the application proves otherwise. A client may send one request, receive no response, and still have the server commit the write. Retrying a payment, reservation, or message with a new identity can repeat the business effect.
An idempotency record should bind these values atomically at the owning service:
- Authenticated caller or tenant scope.
- Operation name and client-supplied key.
- Fingerprint of the normalized request body.
- In-progress or terminal operation state.
- Stored response or stable resource reference.
- Expiry long enough to cover the documented retry window.
The example rejects a reused key with a changed body and returns the first durable result for a valid replay. Production code must persist the key and side effect in one transactional boundary or reconcile them through an operation record.
Lab: recover an uncertain write without charging twice
Place a lost response before or after the durable write, choose a retry policy, and enable or remove the server's replay record. The trace updates write attempts, durable effects, duplicate charges, and the safe recovery decision.
Evolve the contract without surprising clients
Backward compatibility means a supported client can keep using documented behavior. JSON that still parses can nevertheless break when authorization, defaults, ordering, pagination, error codes, or retryability change.
Preferred
Additive evolution
Add optional inputs, ignorable response fields, or new endpoints while preserving old semantics. Test generated clients that may treat unknown enum values strictly.
Intentional break
Versioned migration
Publish a new contract, migration examples, supported SDKs, usage telemetry, and a dated overlap window. Versioning buys migration time; it does not make a poor change safe.
Operational process
Deprecation and sunset
Warn in documentation and response metadata, contact active consumers, measure remaining usage, rehearse rollback, and gate removal on evidence rather than the date alone.
Use URL major versions when routing and discoverability matter most. Header or media-type versions can keep URLs stable but are harder to inspect manually and easy to omit from tooling. Whichever strategy you choose, apply it consistently and publish the support policy.
Protect and operate the API as a product boundary
Enforce trust at the resource owner
- Keep credentials out of browser bundles, URLs, examples, and logs.
- Validate token issuer, audience, expiry, and signature, then authorize tenant, scope, ownership, operation, and current resource state.
- Use API keys for project identification only when that risk model is appropriate; a key alone is not user authorization.
- Apply rate, quota, concurrency, and payload limits by trusted identity. Avoid raw user IDs or keys as unbounded metric labels.
- Rotate signing keys and credentials with overlap, telemetry, and rollback rather than an instantaneous global cutover.
Measure the promise clients experience
R / E / D
Request health
Rate, error ratio, and duration by stable route template, method, version, and outcome.
p50 / p95 / p99
Latency shape
Tail latency reveals timeouts and retries that an average conceals.
One intent
Write safety
Track replay hits, fingerprint conflicts, duplicate reconciliation, and operation age.
Remaining callers
Migration evidence
Measure deprecated-version traffic and owner readiness before sunset.
Alert on user-visible symptoms and sustained capacity risk. Trace one correlation ID through the gateway, service, database or queue, dependency, and response. Redact sensitive fields and keep route templates bounded; raw paths with IDs create high-cardinality telemetry.
Continue with GraphQL, gRPC, and API Paradigms to compare other interface styles after the contract fundamentals are clear.