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

Real-world cases: three versioning failures

  1. Renaming a field without versioning: userName becomes username and every old client breaks at once. Breaking changes need a parallel new version plus a migration window.
  2. 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.
  3. 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:

  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.