Engineering StrategyMay 19, 20264 min read

API Design Decisions You Cannot Undo Later

By Maplecode

API Design Decisions You Cannot Undo Later

Most API design advice concerns style — resource naming, verb selection, whether to use plural nouns. Those matter for consistency and almost never for cost, because renaming an endpoint before anyone uses it is free.

The decisions that matter are the ones that become permanent the moment a third party writes code against them. Once an external client depends on your response shape, your error format or your identifier scheme, changing it is a migration project with a communication plan attached. This is about which decisions fall into that category, and what to do about them before they harden.

Versioning: pick a mechanism, not a philosophy

The argument about whether versions belong in the URL, a header, or a media type consumes more time than it deserves. All three work. What matters is committing to one before your first external consumer, and being clear about what constitutes a breaking change.

That second part is where teams actually get into trouble. Adding an optional field is usually safe. Adding a required request field is not. Changing a field's type is not. Narrowing an enum is not — and neither, less obviously, is widening one, because clients that switch exhaustively on its values will encounter something they do not handle.

URL versioning is the most operationally straightforward: it is visible in logs, easy to route, and unambiguous when debugging. Its cost is that it encourages whole-API version bumps for a single endpoint change. Header versioning is more granular and harder to debug, because the version is invisible in the places you look first when something breaks.

Pagination decided at the wrong moment

Offset pagination — limit and offset — is the obvious first choice, works immediately, and breaks in two specific ways that only appear later.

It is inconsistent under concurrent writes. If a record is inserted while a client is paging, page two contains an item that was on page one, and something is skipped. For a UI this is a minor irritation. For a client synchronising your data into their system, it is silent data loss that surfaces weeks later as a mismatch nobody can explain.

It also degrades. A database asked for offset 100000 generally has to walk the rows it is skipping. Perfectly fast in testing against ten thousand records; a timeout against ten million.

Cursor pagination — an opaque token pointing at a stable sort position — avoids both. It is slightly more work to implement and considerably less pleasant to explore by hand, which is why it loses the initial decision. Switching later means supporting both, because your existing clients have offset logic baked in. If you expect clients to bulk-read your data, choose cursors at the start.

Error responses are part of your contract

Clients write code against your errors. If a validation failure returns a 400 with a JSON body in one endpoint and a 422 with a plain-text message in another, every consumer builds a small pile of special cases, and each of those becomes something you cannot change.

What a client needs is a stable machine-readable code they can branch on, a human-readable message for logs, and field-level detail for validation failures. The message text should be safe to change; the code should not be. Making that distinction explicit in your documentation saves you from a situation where a copy edit breaks a consumer's error handling — which does happen.

One thing worth deciding deliberately: whether a partially successful bulk operation returns 200 with per-item results, or fails atomically. Both are defensible. Changing your mind later is not, because clients have built reconciliation logic on the answer.

Identifiers leak more than you intend

Sequential integer identifiers expose two things. They tell anyone who can see them roughly how many records you have and how fast that is growing, which is commercial information. And they make enumeration trivial, so any endpoint with a weak authorisation check becomes a bulk extraction tool.

Opaque identifiers — UUIDs, or prefixed random strings — avoid both. They cost a little more storage and, if used as a clustered primary key, can hurt insert performance depending on your database. The usual answer is an internal sequential key for storage and an external opaque identifier for the API, which keeps both properties.

Changing identifier format after clients have stored your identifiers in their systems is close to impossible. This is worth ten minutes at the start.

Rate limits are a design feature

An API without rate limits has a rate limit; it is just undocumented and equal to whatever crashes it first. Setting explicit limits is partly about protection and mostly about being predictable — a client that knows the limit can design for it, whereas a client discovering it during a production incident cannot.

Return the limit, remaining quota and reset time in headers on every response rather than only when rejecting. Use 429 with a Retry-After value. Both of these let a well-behaved client back off correctly instead of retrying immediately and making things worse.

What to settle before the first external consumer

Versioning mechanism and your definition of a breaking change. Pagination strategy, chosen against how clients will actually read your data. A single error shape used everywhere. Identifier format. Rate limit semantics, including the headers.

None of these take long. All of them are cheap now and expensive after adoption, which is the only distinction that reliably matters in API design. Style guides can be revised at any time; contracts cannot.

Start here

Let's build what's next.

Tell us where you are and where you want to be. We'll bring the engineering, the AI, and the governance to get you there.