Hermes Wiki
Developer/APIDesign/Versioning/Fundamentals/api-versioning-strategies

API Versioning Strategies

Concept

An API is a contract, and contracts get renegotiated. The moment more than one client depends on your API, you can't freely rename a field, drop an endpoint, or change a status code's meaning without breaking someone. API versioning is the discipline of evolving that contract deliberately — communicating what's changing, giving consumers a way to opt in, and giving yourself a way to eventually retire the old shape.

There are three common places to put the version signal:

  • URI path/v1/orders, /v2/orders. Explicit, cacheable (each version is a distinct URL), trivially routable at the load balancer or gateway, and immediately visible in logs and browser bars. The tradeoff: it implies the whole resource changed even when only one field did, and duplicates routes across the codebase.
  • Header — a custom header (Api-Version: 2026-08-01) or content negotiation (Accept: application/vnd.myapi.v2+json). Keeps URLs stable (better for caching and bookmarking a single canonical resource identity), but hides the version from casual inspection and is easy for clients to omit or get wrong.
  • Query parameter?version=2. Easy to add without touching routing, but easy to omit, and mixes cleanly with the rest of a resource's query semantics only awkwardly.

A fourth, increasingly common approach for evolving APIs gradually rather than in big-bang jumps is date-based versioning (Stripe's model: Api-Version: 2026-08-01), where each version is a snapshot of behavior pinned to the date a breaking change shipped, and the server keeps a compatibility layer mapping each pinned client to the current internal behavior. This decouples "how many breaking changes have there been" (URI /v1, /v2...) from "when did the client last check for one," and lets a provider make many small, additive changes without forcing a version bump for each.

Underneath any of these, semantic versioning discipline (major.minor.patch) is what tells consumers what kind of change happened: additive/backward-compatible changes (new optional field, new endpoint) don't require a version bump at all if the API follows Postel's Law (clients ignore unknown fields); only breaking changes (removed field, changed type, changed required-ness, changed semantics of an existing field) warrant a new major version.

Tradeoffs

Strategy Discoverability Caching Routing simplicity Best for
URI path (/v1/...) High — visible everywhere Clean — distinct URLs Simple, gateway-level Public APIs, coarse major versions
Header / content negotiation Low — hidden Needs Vary header handling Requires app-level dispatch Internal APIs wanting stable URLs
Query parameter Medium Awkward — same URL, different content Simple Quick experiments, rarely a long-term choice
Date-based (Stripe-style) Medium — explicit but opaque without docs Fine, keyed off the header More complex — needs a compatibility-shim layer per version High-change-velocity APIs wanting granular, additive evolution

The deeper tradeoff isn't which mechanism — it's how many versions you're willing to run in production simultaneously. Every live version is an ongoing maintenance and testing burden; the versioning mechanism just decides how visible and how granular that burden is.

When to use / when not to

  • Use URI path versioning for public-facing APIs where explicitness and easy routing at the edge (API gateway, CDN) matter more than URL purity — most consumer-facing REST APIs land here.
  • Use header/date-based versioning for high-velocity internal or platform APIs (Stripe's model) where you want to ship many small additive changes without forcing every client through a major-version migration for each one.
  • Prefer additive, backward-compatible changes by default — new optional fields, new endpoints — and reserve an actual version bump for genuine breaking changes; this is a design discipline, not a versioning-scheme choice, and it's the single biggest lever for reducing how often you need to version at all.
  • Don't version prematurely: a single-consumer internal API you fully control (you own both sides) doesn't need a versioning scheme — coordinate the change and ship both sides together.
  • Don't skip a deprecation policy: every version you ship needs an announced sunset date and a migration guide, or "temporary" v1 support becomes permanent because nobody has a deadline to move off it.

Common pitfall

Versioning the whole API when only one endpoint or one field actually changed. Bumping every route to /v2 because one resource's shape changed forces every consumer — even ones untouched by the change — through a full migration, and it fragments the API into parallel universes that both need to be maintained, tested, and reasoned about. The better default is field-level and endpoint-level compatibility discipline: add fields as optional and additive, deprecate individual fields with clear signaling (a deprecated flag, a sunset header) before removing them, and reserve a full version bump for the rare case where the whole contract's semantics genuinely changed. The second common pitfall is under-communicating deprecation — no sunset date, no migration guide, no automated warning in the response — so a version silently accumulates load-bearing dependents until removing it becomes politically impossible.

Engineering Lens

API versioning is really a question about organizational coordination cost disguised as a technical decision: every version you maintain is a promise to every consumer that depended on it, and the real design work is minimizing how often you have to make and break that promise. The Principal-level framing in a review isn't "which versioning scheme is best" — it's "how do we design changes to rarely need a new version in the first place," which pushes the conversation toward additive-by-default API design, tolerant client parsing, and clear deprecation lifecycles rather than mechanics. This maps directly onto Conway's Law territory (see Team Topologies and Conway's Law): a platform team serving many downstream teams needs a versioning and deprecation discipline that scales with the number of consumers, while a two-service internal integration you control end-to-end usually doesn't need formal versioning at all — recognizing which situation you're in, and not over-engineering the smaller one, is itself a signal of judgment.

Sources

Hermes Wiki