← Back to all articles

How to Version an API: URL, Header, or None

APIPitfalls

Three主流 strategies

StrategyExampleProsCons
URL/v1/usersObvious, debuggable, cacheablePollutes paths
HeaderAccept: application/v1+jsonClean URLsInvisible, hard to debug
NoneAdd-onlySimplestBreaks 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:

  1. Centralise version routing: resolve versions in the gateway or shared middleware so business code never branches on version;
  2. 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";
  3. Automate deprecation: return Deprecation and Sunset headers and tag old-version calls in logs so notifications can be targeted;
  4. Contract tests as a safety net: keep a suite per live version so refactors cannot silently break older clients;
  5. 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.

  1. Add optional fields: extend capability by adding optional fields rather than redefining existing ones, and require callers to ignore unknown fields.
  2. Retire fields in two steps: stop returning them but keep the documentation, watch for a period, then remove the declaration.
  3. Default before requiring: when tightening validation, ship a sensible default first and make it required in the next version.
  4. Allow unknown enum values: adding a value breaks callers that have not updated, so require them to tolerate unknowns and avoid deleting existing values.
  5. 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.