An API is a contract: a promise about what requests a service will accept and what shapes of responses it will return, made to callers who can't see - and shouldn't need to see - anything about the implementation behind it. Designing an API well means designing that promise deliberately, before code, rather than letting it emerge as whatever shape a route handler happened to produce.
An API is a stable contract between a service and its callers, and every design decision - URLs, verbs, status codes, error shapes - exists to make that contract predictable and safe to depend on.
Insight: Callers write code against your response shapes; a change that seems trivial internally (renaming a field, changing a status code) is a breaking change externally if it wasn't part of the documented contract.
When to Use: Designing new endpoints, deciding whether a change is safe to ship without a version bump, choosing between REST-style and RPC-style routes, and reviewing whether an API's error handling is consistent.
Limitations/Trade-offs: A strict, well-documented contract is slower to change than an undocumented one - every improvement has to be weighed against the cost of breaking someone who's already depending on the current shape.
Related Topics: REST resource modeling, HTTP status code semantics, error response standards, API versioning, OpenAPI specification.
The word "design" in API design is doing real work: a resource is the noun an API exposes - an order, a customer, an invoice - and the HTTP methods (GET, POST, PATCH, DELETE) are the verbs applied to it.
That resource-oriented shape isn't an aesthetic choice; it's what lets a caller who has never read your source code correctly guess that DELETE /orders/:id removes an order, without a line of documentation.
An RPC-style alternative - POST /deleteOrder - works too, but it forces every single action to be learned individually, because the URL no longer carries any of the meaning on its own.
A helpful analogy: a well-designed API is a vending machine, not a conversation.
A caller doesn't need to explain what it wants in prose or guess at internal state - it presses a well-labeled button (a resource URL plus a method) and gets a predictable, documented result every time, regardless of what's happening inside the machine.
GET /v1/orders/:id # fetch one orderPOST /v1/orders # create an orderPATCH /v1/orders/:id # partially update an orderDELETE /v1/orders/:id # remove an order
Idempotency is the property that calling an operation twice has the same effect as calling it once - PUT and DELETE are expected to be idempotent by HTTP's own semantics, while POST generally is not, which is why payment endpoints often add an explicit Idempotency-Key header to get that guarantee where the HTTP method alone doesn't provide it.
Once an API ships and a caller writes code against it, the response shape becomes load-bearing infrastructure the API owner doesn't fully control anymore - a mobile app in a user's pocket, or a partner's integration, keeps calling the old shape until someone updates that code, which the API team can't force or even always see happening.
This is the core tension in API design: internally, refactoring is cheap because every caller is a coworker who can update in the same pull request; externally, a "refactor" of a response shape is a breaking change that ships on its own schedule, invisible to the team that made it until support tickets arrive.
Status codes carry meaning independent of the response body, which is what makes them useful for programmatic branching before a client even parses JSON.
// Status code is the first thing a client branches on - before body parsingif (response.status === 404) { // caller can react to "not found" without reading response.body at all} else if (response.status >= 500) { // safe to retry; 4xx generally is not}
That 4xx-versus-5xx distinction is a mechanism, not a convention: HTTP clients, proxies, and retry libraries treat the two ranges differently by default (5xx is often retried automatically; 4xx generally isn't), so returning 500 for a validation error tells intermediate infrastructure to retry a request that will fail identically every time.
A consistent envelope - wrapping success payloads in { data: ... } and errors in { error: ... } - exists for a related reason: it lets a client write one piece of response-handling logic for every endpoint in the API, instead of a special case per route. Without that consistency, every new endpoint is a small integration project for every consumer, because nothing about the previous nineteen endpoints predicts the shape of the twentieth.
Contract stability has a genuine cost, and understanding that cost is what separates deliberate API design from either over-caution or recklessness. A field that's actually unused by any caller can be removed freely; a field that one integration silently depends on cannot, even if it looks unused from inside the codebase - which is why production APIs increasingly instrument response usage (which fields clients actually read) before removing anything.
Versioning exists to let a contract evolve without breaking existing callers: a new major version (/v2/orders) can change response shapes freely, while /v1 keeps serving exactly what it always has until it's formally deprecated on a published timeline. The alternative - mutating /v1 in place - trades a clean URL scheme for an unpredictable contract, which is a worse trade for any API with callers outside the team's direct control.
Clients fetch exactly the fields they need; single endpoint
Caching and rate-limiting are harder; steeper server-side setup
Data-heavy UIs with many client shapes from one backend
Error handling deserves the same contract discipline as success responses, and often gets less of it in practice. A stable machine-readable code field (ORDER_NOT_FOUND) lets clients branch on meaning; a human-readable message string does not, because that string can change with a copy edit and silently break any client that was pattern-matching on its exact text. Error Response Standards covers the RFC 9457 Problem Details shape this stack standardizes on, but the underlying principle applies regardless of format: whatever a client is expected to branch on programmatically has to be exactly as stable as the contract itself.
Modern tooling has shifted some of this discipline from convention to enforcement. An OpenAPI spec, generated from code or hand-written and validated in CI, turns "the contract is whatever the docs say" into "the contract is whatever a machine can check a response against" - catching drift between documentation and implementation before a caller ever notices the mismatch.
"A well-designed API and a working endpoint are the same thing." An endpoint can work today and still be badly designed if its shape is inconsistent with the rest of the API or leaks implementation detail a caller shouldn't depend on.
"Changing an internal implementation never affects the API." It doesn't, as long as the response shape and status codes stay identical - the moment either changes, it's an external, contract-level change regardless of how small the internal diff was.
"REST means CRUD-only, so anything else needs RPC." Non-CRUD actions can still be modeled as resources - POST /orders/:id/cancel treats "cancel" as an action on a resource rather than abandoning resource-oriented design entirely.
"Status code 200 with an error in the body is a harmless shortcut." It defeats every piece of infrastructure that branches on status code (retries, caching, monitoring), forcing every caller to parse the body just to know if a request succeeded.
"Versioning is optional if the team is careful about backward compatibility." Even careful teams eventually need a genuinely breaking change; versioning is what makes that change survivable for existing callers rather than a coordinated flag day.
What makes an API a "contract" rather than just an implementation detail?
Callers outside the team - mobile apps, partner integrations, other services - write code against the exact response shapes and status codes an API returns, and cannot see or influence how that response was produced internally.
Why does resource-oriented URL design matter over RPC-style endpoints?
It gives every endpoint a shared, guessable vocabulary - a caller who understands GET /orders/:id can correctly predict what GET /customers/:id does, without documentation, because the pattern repeats.
How do status codes actually change client behavior, mechanically?
HTTP clients, proxies, and retry libraries branch on the status code range before even parsing the response body - many automatically retry 5xx responses and don't retry 4xx ones, so the code you return determines real infrastructure behavior, not just semantics.
How does a consistent response envelope actually help a client?
It lets a client write one shared piece of code to check data versus error across every endpoint, instead of writing bespoke parsing logic per route because each endpoint's shape is slightly different.
When is it safe to change an API response without a version bump?
Generally only when adding an optional new field that no existing client reads yet - removing a field, renaming one, or changing a field's type or meaning is a breaking change regardless of how small it looks in the diff.
Why shouldn't clients branch on the error `message` text?
Message text is meant for humans and can change with a copy edit at any time; a stable machine-readable code field is the part of the contract meant to be programmatically depended on.
Is REST always the right choice over GraphQL?
No - REST fits CRUD-heavy, cacheable, widely-tooled scenarios well, while GraphQL fits data-heavy UIs that need many different field combinations from one backend; the choice depends on caller shape diversity and caching needs, not one being universally better.
What's the actual cost of a badly designed API, beyond aesthetics?
Every inconsistency (a different envelope shape, an unpredictable status code) becomes integration work every caller has to redo per endpoint, and every "small" breaking change becomes a support incident for whoever depended on the old shape.
Why does idempotency matter for `POST` requests specifically?
POST isn't idempotent by HTTP's own semantics, so a retried request (from a flaky network, for instance) can create a duplicate resource unless the API adds an explicit mechanism, like an Idempotency-Key header, to make retries safe.
How does OpenAPI change the way a contract is enforced?
It turns the contract from a description in a document into a machine-checkable schema - a spec-validated response either matches what was promised or fails a check, catching drift between documentation and actual behavior automatically.
Why does versioning exist if a team is disciplined about backward compatibility?
Even disciplined teams eventually need a change that genuinely can't stay backward-compatible - versioning gives existing callers a stable target (/v1) to keep using while new callers adopt the new shape (/v2), rather than forcing everyone onto a breaking change at once.