Three主流 strategies
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL | /v1/users | Obvious, debuggable, cacheable | Pollutes paths |
| Header | Accept: application/v1+json | Clean URLs | Invisible, hard to debug |
| None | Add-only | Simplest | Breaks if semantics change |
It is not about which one
What matters is the deprecation flow: return a Deprecation header, give old versions a migration window, then retire; and keep backward-compatible additions (optional new fields) from breaking old clients.
Two pitfalls
- Version sprawl: a new version per tiny change explodes maintenance;
- No transition: deleting an endpoint equals a midnight outage for callers.
Real-world cases: three versioning failures
- Renaming a field without versioning:
userNamebecomesusernameand every old client breaks at once. Breaking changes need a parallel new version plus a migration window. - Maintaining too many live versions: v1 through v5 coexist and compatibility code keeps growing. State a policy of supporting the last two major versions and publish a sunset timetable.
- Version in the URL, behaviour in a header: docs and reality diverge, so clients that read the URL get different behaviour. Keep one versioning strategy and make it observable.
FAQ
URL or header versioning? URLs are more visible, easier to debug and cache, and suit most public APIs; headers are "cleaner" but invisible and costlier to troubleshoot. When is a new version required? Only for breaking changes — removing or renaming fields, changing meaning, tightening validation. Adding optional fields does not. How do I notify clients? Announce the sunset with a Sunset response header plus docs and console alerts. How long should deprecation last? Three to six months, monitoring per-version traffic until it reaches zero.
Mechanics that make versioning work
Picking a strategy is the easy part; these are what decide the outcome:
- Centralise version routing: resolve versions in the gateway or shared middleware so business code never branches on version;
- Make traffic visible: instrument request volume, error rate and caller distribution per version — before sunsetting you must be able to answer "who still calls this";
- Automate deprecation: return
DeprecationandSunsetheaders and tag old-version calls in logs so notifications can be targeted; - Contract tests as a safety net: keep a suite per live version so refactors cannot silently break older clients;
- Docs from one source: generate version notes from the API definition, or docs will claim v2 while the code ships v3.
With all five, sunsetting becomes a routine release; without them, old versions linger and slowly consume your maintenance budget.
Pairing with non-breaking evolution
The best way to need fewer versions is to make as many changes non-breaking as possible.
- Add optional fields: extend capability by adding optional fields rather than redefining existing ones, and require callers to ignore unknown fields.
- Retire fields in two steps: stop returning them but keep the documentation, watch for a period, then remove the declaration.
- Default before requiring: when tightening validation, ship a sensible default first and make it required in the next version.
- Allow unknown enum values: adding a value breaks callers that have not updated, so require them to tolerate unknowns and avoid deleting existing values.
- Review schema changes: bring API definition edits into code review with an explicit compatibility note to stop breaking changes at the source.
When non-breaking changes are the norm, version churn falls and maintenance cost with it — cheaper than managing many parallel versions.