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.
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.