APIs become contracts faster than teams expect
A user interface can be redesigned. Internal code can be refactored. A public or partner-facing API is different: once another system depends on its behaviour, accidental decisions become contracts. A misspelled field name can survive for years. An ambiguous error model can be copied into dozens of integrations. A pagination rule that works for one hundred records can become impossible to change after customers build workflows around it.
That is why we treat API design as product design from day one. The goal is not theoretical purity. It is to create a contract that is predictable for consumers and flexible enough for the provider to evolve.
Design from workflows, not database tables
One of the most common API mistakes is exposing the persistence model directly. Tables become resources, internal IDs become public identifiers, and implementation relationships leak into the contract. The result feels efficient initially because little translation is required, but future database changes become breaking API changes.
We prefer to model APIs around domain concepts and consumer workflows. The API should communicate what the business object means, which state transitions are valid, and what clients need to accomplish - not how the current schema happens to store it.
This does not require a heavy abstraction layer. It requires the discipline to decide which details are part of the contract and which are internal.
Consistency is more valuable than cleverness
Consumers learn an API by pattern recognition. If identifiers, timestamps, pagination, filters, sorting, errors, and naming conventions work the same way across endpoints, integration becomes faster and safer.
Choose conventions early and document them. Decide how resources are named, how timestamps include timezone information, how optional values are represented, whether enums are open to future values, how list endpoints expose metadata, and how correlation identifiers are returned.
Inconsistent APIs create hidden cognitive cost. Every exception becomes another rule client developers must remember.
Make errors actionable
A 400 response with “invalid request” is technically an error model and practically useless. Good errors help machines categorise the problem and humans resolve it. We usually want a stable error code, human-readable message, relevant field or context when safe, and a request or correlation identifier for support.
Differentiate authentication failures, authorisation failures, validation errors, conflicts, rate limits, missing resources, and transient server problems. Clients often need different behaviour for each category.
Error messages are also part of observability. Support teams should be able to connect a customer-visible failure to logs or traces without asking the customer to reproduce it five times.
Idempotency should be designed before retries are needed
Distributed systems retry. Mobile networks fail. Load balancers time out. Queues redeliver. Clients cannot always know whether a request reached the server. If a create or payment-like operation is not idempotent, a normal retry can create duplicate effects.
For operations where duplication matters, support idempotency keys or an equivalent domain mechanism. Store enough information to return a consistent result for a repeated request and define the retention period and scoping rules clearly.
Idempotency is much easier to add before external consumers depend on unsafe behaviour.
Pagination and filtering need a growth plan
List endpoints are deceptively simple. Offset pagination can work well for small, stable datasets, but it becomes expensive or inconsistent under heavy mutation and large offsets. Cursor-based pagination is often better for feeds and large collections, but it requires careful ordering semantics.
The right choice depends on the use case. What matters is that pagination is explicit, deterministic, and documented. The same applies to filtering and sorting: define supported fields, operators, default order, maximum page size, and behaviour for invalid parameters.
Avoid returning unbounded collections. The client that asks for “all records” during testing may become the client that takes production down two years later.
Version only when compatibility cannot be preserved
Versioning is not a substitute for compatibility discipline. If every additive field triggers a new API version, the provider creates unnecessary migration work. Many changes can be introduced without breaking clients: adding optional response fields, adding endpoints, accepting new optional request fields, or expanding capabilities behind existing contracts.
Breaking changes require a strategy. URI versions, headers, media types, or dated versions can all work if applied consistently. More important than the mechanism is the lifecycle: how long versions are supported, how deprecations are announced, how usage is measured, and what tooling helps customers migrate.
We prefer to make compatibility the default and versioning the exception.
Be careful with enums and “closed world” assumptions
A provider may think an enum has three permanent values. Two years later, the business adds a fourth and strict clients fail. The same problem occurs when consumers assume every field they see will always be present or that unknown response properties are errors.
API documentation should tell clients which parts of the contract are extensible. Where possible, consumer implementations should tolerate additive fields and unknown enum values gracefully. On the provider side, breaking changes should not be smuggled into fields that clients reasonably treat as closed sets.
Longevity requires both sides to understand where future change is allowed.
Documentation is part of the interface
An OpenAPI document is valuable, but generated schema alone is not enough. Good API documentation explains authentication, common workflows, error handling, pagination, idempotency, rate limits, webhooks, retries, examples, and environment differences.
Examples should reflect real workflows rather than isolated endpoint calls. A customer trying to integrate usually wants to know how to accomplish a task, what sequence of operations is expected, which states are asynchronous, and how to recover from failure.
Documentation should be versioned with the API and reviewed as part of delivery. If the implementation changes but the docs do not, the API is effectively broken.
Operational visibility belongs in API design
Long-lived APIs need more than a good schema. They need rate-limit visibility, request identifiers, usage metrics, latency and error monitoring, consumer-level diagnostics, and a way to identify deprecated behaviour still in use.
For webhooks, track delivery attempts, signatures, response codes, retry schedules, and dead-letter conditions. For partner integrations, provide enough observability that both teams can determine whether a problem occurred before, inside, or after the API boundary.
A design that is easy to call but impossible to support will become expensive at scale.
The principle underneath all the others
The core principle is to preserve optionality for the provider without making the consumer guess. Stable naming, explicit contracts, additive evolution, actionable errors, safe retries, bounded collections, disciplined versioning, and living documentation all serve that goal.
Teams rarely regret spending an extra day making an API coherent before external adoption. They frequently regret exposing an implementation shortcut that later becomes permanent.
The debt in a poorly designed API is unusually persistent because someone else owns the code that depends on it. Design accordingly.