API versioning is the strategy for evolving an API without breaking the clients already using it. Once published, an API becomes a contract: renaming a field or reshaping a response can shatter many integrations at once. The goal of versioning is to distinguish changes you can make safely from changes that require a new version, and to transition the latter without breaking anyone.
Where to carry the version: three approaches
There are three families, differing in where the version rides on the request.
- URI path (
/v1/users): The most common and intuitive. The version is visible in the URL and easy to test with a browser orcurl. The downside is that the same resource gets a different URL per version, which grates against the REST purist view that a URI should identify a resource. - Header (
Accept: application/vnd.example.v2+jsonor a customX-API-Version: 2): The URL stays stable and the version is negotiated. It's clean but invisible, which complicates debugging and docs; omit the version and you silently fall back to a default. - Query parameter (
/users?version=2): Easy to implement, but it muddies cache keys and forces you to define exactly what happens when the parameter is absent.
GET /v1/users/u_92311 HTTP/1.1
Host: api.example.com
Accept: application/vnd.example.v2+json
X-API-Version: 2
Media-type versioning
A refined form of the header approach is media-type (content-negotiation) versioning. The client asks for a representation version via a vendor media type in Accept (application/vnd.example.v2+json), and the server responds through content negotiation. Because it keeps the URI a pure resource identifier, it's theoretically the most 'RESTful' — but in practice its learning curve and thin tooling support keep it far less common than path versioning.
When to bump the version
The core principle: bump the version only for a breaking change. Define clearly, as a team, what counts as breaking and what doesn't.
- Backward compatible (keep the version): adding a new endpoint, adding a new field to a response, adding a new optional request parameter, adding a new error code.
- Breaking (bump the version): removing or renaming a field, changing a field's type, adding a required parameter, reshaping the response, changing error semantics, changing default behavior.
- Robustness principle: if clients ignore fields they don't recognize, then a server 'adding a field' stops being a breaking change.
Announcing deprecation: Deprecation and Sunset
When retiring an old version, don't cut it off without warning — announce it with standard headers. The Deprecation header marks a response (or endpoint) as deprecated, and the Sunset header states the date/time when responses will actually stop. Pointing at migration docs with a Link header is even friendlier.
HTTP/1.1 200 OK
Deprecation: true
Sunset: Wed, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/docs/migrating-to-v2>; rel="deprecation"
Content-Type: application/json
Practical guidance — For most teams, URI path versioning (/v1, /v2) is the most practical: it's visible and easy to cache, route, document, and tool. But don't version-happy. Splitting into /v2, /v3 too often explodes the cost of maintaining several versions at once. The healthy pattern is to keep backward compatibility as long as possible so one major version lives a long time, opening a new version only for a truly unavoidable overhaul. And whenever you do ship a new version, always pair it with a clear retirement timeline (Deprecation/Sunset) and a migration guide for the old one.