What you'll learn
Quick Answer
URL versioning (/v1/students) is the most common and the easiest to debug. Only version when a change is genuinely breaking — adding a field is not. Have a deprecation plan before you ship v2.
What counts as a breaking change
Worth being precise, because over-versioning is as costly as under-versioning.
Breaking: removing or renaming a field; changing a field's type; making an optional parameter required; changing the meaning of an existing value; removing an endpoint; changing a success status code.
Not breaking: adding a new optional field to a response; adding a new endpoint; adding an optional query parameter; adding a new enum value if clients were told to tolerate unknown values.
The rule of thumb: if existing client code stops working, it is breaking. Adding a field is safe because a well-written client ignores fields it does not know — which is worth stating explicitly in your documentation, since it gives you room to grow.
That enum caveat is real. Adding a new order status breaks any client with an exhaustive switch that throws on unknown values.
The three approaches
URL path — /v1/students. The most common. Immediately visible in logs, easy to test in a browser, trivially routable to different services. Purists object that the URL should identify a resource rather than a representation, which is true and rarely decisive.
Custom header — API-Version: 2. Keeps URLs clean and stable. The cost is that you cannot try a version by pasting a URL, and it is easier to forget the header exists when debugging.
Accept header — Accept: application/vnd.example.v2+json. The most technically correct form of content negotiation, and the least convenient. Rare outside large public APIs.
Query parameter — ?version=2. Easy, but mixes API structure with request data and is easy to omit accidentally.
For most projects, URL versioning is the right default. It is the most debuggable, and debuggability matters more than purity when something is broken at 2am.
The best version is the one you never ship
Every additional version is code you maintain, test and support indefinitely. Two live versions means every bug fix needs consideration in both.
So before creating v2, check whether the change can be made additively:
- Add rather than replace. Need a different name format? Add
full_namealongsidename, mark the old one deprecated in the docs, and remove it much later. - Expand then contract. Add the new field, migrate clients, then remove the old one in a version bump long afterwards.
- Use optional parameters to opt into new behaviour, keeping the default unchanged.
Most changes that feel like they need a new version do not. Versioning is for genuine structural change, not for every improvement.
Retiring a version
Shipping v2 is easy. Turning off v1 is the hard part, and it is where most APIs accumulate permanent debt.
A workable sequence:
- Announce with a concrete date, not "soon". Document exactly what changed and how to migrate.
- Signal in the responses — a
Deprecationheader and aSunsetheader with the retirement date. Clients that log warnings will surface it. - Measure who is still calling it. You cannot retire safely without knowing who is affected, and this is exactly what per-version logging is for.
- Contact remaining users directly if you can identify them.
- Consider a brownout — return errors for a short window before permanent removal. Teams that ignore emails notice a failing test.
Give a realistic window. Six to twelve months is normal for a public API; internal APIs can move faster because you know every caller.
Practical guidance
- Ship v1 from the start. Adding a version prefix later is itself a breaking change. It costs nothing when nobody is using the API yet.
- Version the API, not each endpoint. Per-endpoint versions produce combinations nobody can reason about.
- Whole numbers only.
/v1,/v2. Semantic versioning belongs on libraries; an API consumer only cares whether their code still works. - Share the implementation. Translate v1 requests into the v2 handler rather than maintaining two full stacks, or you will fix each bug twice.
- Log the version on every request. Without it, you can never safely retire anything.
See designing a REST API for the decisions that reduce how often you need a new version at all.
