Module 04 · API fundamentals · Lesson 5 of 15
API versioning and evolution
Most changes should never need a version. The additive-change rules that keep clients working, the three places a version can live when you must break something, and how to retire old versions without a cliff.
Why it matters
The day after an API has a second consumer, you can no longer change it freely. Versioning is how you keep improving the API without breaking the people who built on yesterday's. Done badly it produces a zoo of versions nobody dares remove; done well it is mostly invisible, because the vast majority of changes turn out not to need a version at all.
The idea
Separate two questions: which changes are compatible (and so need no version), and where the version lives for the changes that are not.
Most changes are compatible if you follow the rules
| Compatible (no version needed) | Breaking (needs a version) |
|---|---|
| Adding a new endpoint | Removing or renaming an endpoint |
| Adding an optional request field | Adding a required request field |
| Adding a response field | Removing or renaming a response field |
| Adding a new enum value the client can ignore | Changing the meaning or type of a field |
| Loosening validation | Tightening validation on existing input |
| Adding a new error code (in the documented envelope) | Changing status codes for existing outcomes |
Two habits make additive evolution safe. Clients must ignore unknown fields (tolerant readers), and servers must never reuse a removed name for something different. With those in place, an API can grow for years on one version.
Where the version lives, when you need one
- 1
In the URL: /v2/orders
The most visible and the most common. Easy to route, cache and document; a version is a whole new surface, which encourages big-bang rewrites. - 2
In a header: Accept: application/vnd.example.v2+json or API-Version: 2024-06-01
Keeps URLs stable and lets clients opt in per request. Harder to see in logs and browsers; date-based versions (Stripe's approach) let you pin behaviour per account and evolve continuously. - 3
In the schema: new fields and types alongside old
GraphQL and protobuf avoid global versions by deprecating fields individually; the schema is the version. Works when the API is a typed graph and clients can see deprecation metadata.
Retiring versions
A version with no sunset date is permanent. Announce deprecation with a date (6 to 12 months for public APIs), return a Deprecation or Sunset header on old-version responses so automated clients notice, measure traffic per version and contact the holdouts, then return 410 Gone after the date. Internally, two active versions is a healthy maximum; three means something has stalled.
Trade-offs
URL versions are discoverable and encourage coarse, infrequent breaks; header versions are flexible and encourage many small, less visible ones. Running two versions doubles test surface and support load for the window, which is why the first job is avoiding breaks altogether. Deprecation deadlines annoy a few consumers and save everyone else from carrying dead code forever.
In an interview
Lead with the compatibility rules, not the version scheme: "Additive changes only, clients ignore unknown fields, so most evolution needs no version. For a genuine break I would add /v2, run v1 as an adapter over the same model, send Sunset headers, and remove v1 after six months once traffic is near zero." That shows you have retired a version, which is rarer than having created one.
Check yourself
A team adds a new optional field to a response and a new optional query parameter to a request. A consumer's client crashes because it rejects unknown fields. Who should change?