Pavan Rangani

HomeBlogVersioning an API Without Breaking Everyone

Versioning an API Without Breaking Everyone

By Pavan Rangani · August 18, 2026 · Architecture

Versioning an API Without Breaking Everyone

Ask about API versioning and you get a debate about URLs versus headers. That debate is the least important part. The real problem is harder and more human: how do you change an API when other people’s software depends on it, software you do not control and cannot redeploy? Once someone integrates against your API, you have made a promise, and versioning is the discipline of evolving without breaking that promise.

The only definition that matters: what is a breaking change?

Before any versioning strategy, you need a precise sense of what actually breaks a client, because the intuition is unreliable. Some changes that feel large are safe, and some that feel tiny are catastrophic.

Safe (additive) changes: adding a new endpoint, adding an optional field to a request, adding a field to a response. A well-written client ignores response fields it does not recognise, so adding one breaks nothing. This is the foundation of evolving an API without versioning at all.

Breaking changes: removing or renaming a field, changing a field’s type, making a previously optional request field required, changing an error code, tightening validation. Every one of these can crash a client that was working yesterday. Renaming a field from userName to username feels trivial and will break every client reading the old name.

The rule that follows is powerful: additive changes never need a new version. If you only ever add, existing clients keep working forever and you never version at all. The entire art of long-lived APIs is arranging to change by adding rather than by modifying.

Interconnected systems representing clients depending on an API
Versioning is about the clients you cannot redeploy — additive changes keep the promise without a new version.

Where the version lives, briefly

Since people always ask: the common options are a version in the URL path (/v2/orders), in a header (Accept: application/vnd.api.v2+json), or in a query parameter. URL versioning is the most visible and the easiest to route, cache, and explain — you can paste it into a browser. Header versioning is purist and keeps URLs stable but is invisible and harder to test by hand.

Pick one and be consistent; the choice matters far less than people spend arguing about it. URL versioning wins most of the time for the plain reason that it is obvious, and an API that is easy to understand gets integrated correctly more often. What actually matters is what you do between versions, not the syntax of the version marker.

Additive evolution: how to avoid v2 for years

The best version is the one you never had to create. Because additive changes are safe, you can evolve an API for a very long time without a new version, if you design for it.

Need to change a field’s type? Do not change it — add a new field alongside the old one and populate both, letting old clients read the old field and new clients read the new. Need to change behaviour? Gate it behind an optional parameter that defaults to the old behaviour, so nothing changes for anyone who does not ask for the new one. Need to remove a field? Stop documenting it, watch whether anyone still sends or reads it, and only then plan its removal. This is the same expand-then-contract discipline that makes database changes safe, described in our zero-downtime migrations guide — add the new, migrate onto it, retire the old, never change in place.

Done well, a v2 becomes a rare event reserved for a genuine redesign, not the routine response to every change. Most APIs that sprout a v2, v3, and v4 in quick succession did so because they modified instead of adding, not because the domain demanded it.

When you truly need a new version, run both

Sometimes a change is genuinely, unavoidably breaking — a fundamental reshape of a core resource. Then you introduce a new version and, critically, keep the old one running. The point of versioning is that clients migrate on their own schedule, not yours. Shipping v2 and immediately shutting off v1 is not versioning; it is a breaking change with extra steps.

The sustainable way to run two versions is to avoid duplicating your whole stack. Translate at the edge: keep one internal model, and have the old version’s layer adapt requests and responses to and from it. That way v1 becomes a thin translation shim over the same core logic, rather than a second codebase you maintain in parallel forever. The alternative — two full implementations drifting apart — is how versioning becomes the thing that slows every future change.

Catch breaking changes before they ship, not after

The whole discipline above depends on knowing whether a change is breaking, and human judgment is unreliable at that — the whole point of the earlier list was that intuition gets it wrong. So the real safeguard is not a careful reviewer; it is automation that fails the build when a response shape changes incompatibly, before the change reaches a single client.

The first line of defense is schema diffing. If your API has an OpenAPI specification, tools can compare the new spec against the old and flag any change that breaks compatibility — a removed field, a tightened type, a newly-required parameter. Wired into CI, this turns “did we just break someone?” from a question a human might forget to ask into a check that runs on every pull request. A removed field becomes a red build with a specific message, not an incident three days later when a client’s integration falls over.

The stronger technique is contract testing, particularly consumer-driven contracts. Each consumer of your API declares, in an executable form, exactly what it depends on — these endpoints, these fields, these types. Those contracts run against your API in CI, so if a change would break a real consumer’s actual expectations, your build fails with the name of the consumer it would have broken. This is far more precise than a general compatibility check, because it tests against what clients genuinely use rather than against every theoretical change, and it means you can safely remove a field the moment no contract still depends on it — the data-driven version of the “watch whether anyone still reads it” advice above.

The reason this matters so much for versioning is that it changes the economics of additive evolution. When you can prove, automatically, that a change breaks nobody, you can make far more changes without a new version — confidently, because a machine verified it rather than a human hoping. The teams that evolve an API for years without a v2 are usually the ones with this safety net; the teams that sprout versions constantly are often the ones changing things blind and reaching for a new version out of fear. The automation is what makes fearless additive change possible.

Deprecation is a process, not an announcement

Retiring an old version is where good intentions go to die, because there is always a client still using it. A version with one important customer on it cannot simply be switched off, and “we announced it six months ago” is not a defence when their integration breaks.

Retirement is a sequence, not a moment. Announce the deprecation with a real date. Signal it in the API itself with a Deprecation header and a Sunset date, so the information reaches the developers who never read the changelog. Instrument usage so you know exactly who is still calling the old version and can talk to them directly. Then, as the date nears, consider brownouts — briefly disabling the old version for short windows — which surfaces the clients that swore they had migrated but had not. Only after usage has genuinely fallen to near zero do you turn it off.

The honest framing is that every version you ship is a maintenance commitment measured in years. That is the real argument for additive evolution: the cheapest version to maintain is the one you never created, because you found a way to add instead of break. The clients you never disrupted are the ones who keep trusting the API — and idempotent, forgiving endpoints, of the kind described in our idempotency guide, are part of the same promise: an API that is safe to depend on.

← Back to all articles