In one line: Version an API only when a breaking change can’t be avoided; until then, add things without removing or renaming anything, and old clients keep working.
Explanation
What versioning is for
An API is a contract. Clients you don’t control (mobile apps, partner systems) are built against it and upgrade on their own schedule. Versioning gives each version of the contract an identifier, so a client can keep calling the one it was built for while new clients use the new one.
For example, v1 of GET /users returns name and email; v2 also returns phone number and role, in a shape v1 clients don’t expect.
Breaking vs non-breaking changes
A breaking change makes an existing, correct client fail or misbehave:
- removing or renaming a resource, method, parameter, header or field;
- changing a field’s meaning, type or format (a date string becomes a timestamp);
- changing behaviour or business logic clients rely on;
- tightening security or authentication requirements;
- adding a new required request parameter.
A non-breaking change is additive: a new endpoint, a new optional parameter, a new response field. Well-behaved clients ignore fields they don’t know.
Versioning has a cost: every live version must be maintained, tested, secured and documented; clients get confused about which to use; and clients must know the exact identifier to find anything. So the first rule is design so you rarely need a new version: consistent resource names, standard HTTP methods and media types, optional features as query parameters or headers, and meaningful status codes and errors.
Where the version goes
| Strategy | Example | For | Against |
|---|---|---|---|
| URI path | GET https://example.com/v1/users | Visible, simple to route, easy to test in a browser and to cache | Clutters URIs; the same resource gets several URIs, against the idea that a URI names one resource |
| Query parameter | GET https://example.com/users?v=1 | Simple; easy to default when missing | Mixes versioning with filtering parameters; easy to forget |
| Custom header | X-API-Version: 1 | Keeps URIs clean and stable | Hidden; caches must vary on the header; harder to try by hand |
| Media type (content negotiation) | Accept: application/json;version=1 | Uses HTTP as designed; can version each representation | Most complex for clients and docs; caches must vary on Accept |
| Body | { "version": 1, "users": [...] } | Fine-grained, explicit | Server must parse the body before routing; changes the payload |
No strategy is the best one. URI versioning is the most common in public APIs because it’s visible; header and media-type versioning keep URIs clean. Pick one and use it for the whole API.
How to change an API safely
- Prefer additive changes. Add resources, parameters, headers or fields; don’t remove or rename existing ones.
- Default the new. New optional inputs get defaults so old requests still mean the same thing.
- Alias renames. If a name must change, accept both names for a while and mark the old one deprecated.
- Bump the version only for a breaking change, and bump the major number: under semantic versioning (
MAJOR.MINOR.PATCH), major is breaking, minor is additive, patch is a fix. - Publish a support policy: how often versions ship, how long old ones live, how clients are told. Tell clients about a deprecation ahead of time, before removing the old version.
- Document every version with a changelog and examples, and test every live version, including that the old ones still behave as before.
- Secure every live version. An old version that nobody patches is an open door.
Example
The users resource gains phone and role.
- Additive: add both fields to the existing response.
v1clients ignore the new fields. No new version. - Breaking: split
nameintofirstNameandlastName.v1clients readnameand get nothing, so this ships as/v2/userswhile/v1/userskeeps returningnameuntil its retirement date.
Common mistakes
- Creating a new version for an additive change.
- Changing a field’s type or meaning in place, without a version.
- Mixing strategies (some endpoints versioned in the path, others in a header).
- Versioning without a deprecation policy, so old versions live forever and nobody patches them.
- Forgetting caches: with header or media-type versioning, the response must send
Varyon that header, or a cache can serve av1body to av2client.
Resources I used
- Article: 4 API Versioning Best Practices in 2023 by Barry Ugochukwu (KodeKloud), the whole article: when to version, URI, header and body strategies, and the backward-compatibility, documentation, policy and security practices.
- Video: Versioning Your API by Udacity, the whole video.
Revision
Key takeaways
- Version only for breaking changes; additive changes keep the same version.
- Breaking means removing, renaming, or changing the meaning or type of anything clients use, or adding a required input.
- The version can live in the URI, a query parameter, a header, the media type or the body; choose one and use it everywhere.
- Every live version costs maintenance, testing and security: publish a deprecation policy.
Check yourself
Is adding a new field to a JSON response a breaking change?
Usually not. Clients should ignore fields they don’t know, so an added response field is additive. Adding a new required request field is breaking.
Give one advantage and one drawback of putting the version in the URI path.
Advantage: visible and simple to route, test and cache. Drawback: the same resource gets several URIs (/v1/users/7, /v2/users/7), and the URI space gets cluttered.
With header versioning, what must a cached response tell caches?
It must send Vary: X-API-Version (or Vary: Accept for media-type versioning), so a cache stores a separate copy per version and doesn’t serve a v1 body to a v2 client.
You must rename name to fullName. How do you do it without breaking anyone right away?
Return both fields (alias), document name as deprecated with a removal date, and remove name only in the next major version, after clients have had time to switch.
History
- 2026-11-13: First notes, from the KodeKloud versioning article.