API versioning strategies that survive breaking changes
URL versions, headers, dates, or no versions at all: how the four common strategies handle a breaking change, and the discipline that matters more.
- Any versioning scheme survives if you detect breaking changes before merge and count affected consumers before shipping.
- URL paths, headers, date pinning, and additive-only evolution trade explicitness against operational cost in different places.
- A sensible default: a coarse URL version you plan never to bump, additive evolution inside it, and a spec diff gate in CI.
Every versioning debate is really a debate about breaking changes: who absorbs the cost when the interface has to move. Pick any scheme you like; the teams that ship stable APIs are distinguished not by where they put the version number but by whether they can detect a breaking change before it merges and count who it affects before it ships. Get those two capabilities and any of the four common strategies works. Skip them and no URL scheme will save you.
First, know what actually breaks
A breaking change is anything that invalidates a request that used to succeed or a parse that used to work. The obvious ones: removing an endpoint or field, renaming anything, changing a type, making an optional parameter required, tightening validation, changing auth requirements. The sneaky ones cause more incidents: reordering enum meanings, changing default values, altering error shapes that clients switch on, tightening rate limits, and changing the semantics of a field while leaving its name and type untouched. Additive changes, new endpoints, new optional fields, new enum values where clients were told to expect unknowns, are generally safe. "Generally" is doing work in that sentence: a client that fails on unknown fields turns an additive change into an outage, which is why the contract should say what consumers may assume.
The four strategies
| Strategy | Granularity | Visibility | Operational cost |
|---|---|---|---|
URL path (/v1/) | Whole API | High | Low, until the v2 project |
| Header or media type | Per request | Low | Medium |
| Date-based pinning | Per consumer | Medium | High: every dated behavior is code |
| Additive-only, no version | n/a | High | Low, until a true break |
URL path versions (/v1/payments). Explicit, cacheable, visible in every log line, and trivially routable to different backends. The cost is coarseness: bumping /v1 to /v2 invalidates the whole surface at once, so teams defer it for years, and when v2 finally lands it carries five accumulated breaks and a migration project. Stripe kept /v1 in its paths for over a decade while evolving constantly; the path version ended up as decoration over a finer-grained mechanism.
Header or media-type versions (Api-Version: 3, application/vnd.acme.v3+json). Cleaner URLs and per-request granularity, at the cost of invisibility: versions vanish from browser tests, casual curls, and most log pipelines, and every intermediate cache needs to be told to vary on the header. Works best for APIs whose consumers are all disciplined server-side integrations.
Date-based pinning. Each consumer is pinned to the API as it behaved on a date, and upgrades by changing one value. The provider keeps transformation layers that replay old shapes onto the current implementation. This is the finest-grained option and the friendliest to consumers, since each account migrates on its own schedule. The price is operational: every dated behavior you have ever shipped is now code you maintain, and the transformation stack needs tests of its own.
No explicit version, additive-only evolution. The strategy of most internal platforms and GraphQL deployments: never break, only add, deprecate loudly, remove after consumers hit zero. This is less naive than it sounds and it quietly underpins every other strategy too, because even versioned APIs should be evolving additively within a version. It fails only when a true break is unavoidable, which is where the next section comes in.
The discipline underneath
Whatever the scheme, the stable teams run the same pipeline:
- Diff every change against the interface, mechanically. Breaking-change detection belongs in CI, comparing the generated spec of this commit against the last, not in a reviewer's memory of what clients depend on. Humans catch renames; they miss a validator quietly tightening a string to an enum. Machines catch both.
- Classify against the contract, per audience. The same diff can be breaking for a partner and irrelevant internally. A contract that records which fields each audience was promised turns "is this breaking" from a debate into a lookup, and different audiences can carry different policies: block for partners, warn internally.
- Count consumers before deciding. "Who calls this" should be a query over real traffic, not a guess. A breaking change to an endpoint with zero callers in 90 days is a deletion; the same change on an endpoint with three partner integrations is a project. Deprecate with usage data and the removal date defends itself.
- Announce in a changelog consumers can subscribe to. Every consequential diff, flagged as breaking or not, published where consumers already look. Silent changes are how trust dies even when nothing technically broke.
Note that agents raise the stakes on all four. An AI agent calling your MCP server re-reads the interface constantly and exercises it exhaustively, so an unannounced rename fails in a customer conversation within hours, not in the next quarterly integration review.
A default that works
For a new external API today: put a coarse version in the URL and plan to never bump it, evolve additively within it, run spec diffing with per-audience contracts in CI so breaks are caught and counted before merge, and reserve date pinning for the day you have enough consumers to justify the machinery. The version number is the tax code on the door. The diff gate, the consumer counts, and the changelog are the actual building.
FAQ
What counts as a breaking API change?
Anything that invalidates a request that used to succeed or a parse that used to work: removing or renaming endpoints and fields, changing types, making optional parameters required, tightening validation, or changing auth requirements.
Which API versioning strategy is best?
Any of the four common ones works if you can detect breaking changes before merge and count affected consumers before shipping. A sensible default is a coarse URL version you plan never to bump, with additive-only evolution inside it.
Are additive changes always safe?
Generally, but not absolutely. A client that fails on unknown fields turns an additive change into an outage, which is why the contract should state what consumers may assume about new fields and enum values.
Ship notes, monthly
One email with what shipped and what we learned. Unsubscribe anytime.