Ask five engineers whether /api/v1/ or an Accept header is the "correct" way to version an API and you'll get five confident, contradictory answers. That debate is mostly noise. The actual failure I see in production APIs almost never comes from picking the wrong versioning mechanism — it comes from never defining what counts as a breaking change in the first place.
Define the contract before you version it
A version number protects a contract. If the contract itself isn't defined, versioning just gives you a false sense of safety while you break integrations anyway.
Before shipping v1 of any API I build, the contract explicitly states:
- Adding a new optional field to a response is not breaking.
- Adding a new required field to a request is breaking.
- Changing the type of an existing field is breaking, always, no exceptions.
- Removing a field, even one "nobody uses," is breaking — because you don't actually know that.
- Changing the meaning of an existing field without changing its name or type is the worst kind of breaking change, because it fails silently.
Write this down once, put it in your API documentation, and every future change gets checked against it instead of argued about from scratch.
The versioning mechanism, briefly
Once the contract is defined, pick a mechanism and move on:
- URL versioning (
/v1/,/v2/) is the most operationally simple — you can route entire versions to different code paths or even different deployments. It's also the most visible to API consumers, which is usually a feature, not a bug. - Header versioning is more "correct" in a REST-purist sense and lets you version resources independently, but it's harder to debug (nobody can see the version in a browser bar) and harder to route at the infrastructure layer.
For the overwhelming majority of B2B and internal APIs I build, URL versioning wins purely on operational simplicity. Save header versioning for APIs with many independent resource types evolving on different timelines — which is rarer than the debate suggests.
The part everyone skips: a deprecation policy with teeth
A version strategy without a deprecation policy just means you accumulate versions forever. Before v1 ships, decide and publish:
- How long a version is supported after v2 ships (I default to 12 months minimum for external APIs).
- What the deprecation notice looks like — a response header, an email to registered API consumers, both.
- What happens when the sunset date arrives — hard cutoff, or a slow-throttle that makes the cost of staying on the old version visible before it becomes unavailable.
What actually breaks integrations in practice
In every API incident I've been called in to diagnose, the root cause was never "we used the wrong versioning scheme." It was an "additive" change that turned out to be breaking for a consumer with brittle client code, deployed without anyone checking the contract definition first. Get the contract right, and the mechanism you hang it on stops mattering nearly as much as the debate suggests.
Building something and want a second opinion?
Thirty minutes, no pitch deck — bring the problem.
Book a discovery call