# API Versioning

An API is a promise to callers you often can't see or contact. The moment a client depends on a response shape, that shape is load-bearing — changing it breaks code you don't control and may never hear about until it's in production. Versioning is the set of conventions for making changes anyway, without breaking everyone who already integrated.

## 1. Breaking vs. Non-Breaking Changes

The entire discipline rests on being able to classify a change correctly.

**Non-breaking (safe to ship without a version bump):**
- Adding a new optional field to a response
- Adding a new endpoint
- Adding a new optional request parameter with a sensible default
- Adding a new enum value, *if* clients are contractually required to handle unknown values gracefully
- Relaxing a validation rule (accepting more input than before)

**Breaking (requires a version bump or a migration path):**
- Removing or renaming a field
- Changing a field's type (`"id": 42` -> `"id": "42"`)
- Changing the meaning of a field without changing its name
- Adding a *required* request parameter
- Changing a status code an existing case returns
- Tightening validation (rejecting input that used to be accepted)
- Changing pagination defaults, sort order, or error shapes

```json
// Before
{ "user": { "id": 42, "name": "Ada" } }

// Non-breaking: added field
{ "user": { "id": 42, "name": "Ada", "email": "ada@example.com" } }

// Breaking: type changed on an existing field
{ "user": { "id": "42", "name": "Ada" } }
```

A useful test: **could a client written against the old contract, with reasonable-but-not-paranoid parsing, keep working unmodified?** If yes, it's additive. If the client would throw, silently misbehave, or receive different data under the same call, it's breaking.

## 2. URI Path Versioning

The most visible and most common strategy — the version is baked into the URL:

```
GET /v1/orders/42
GET /v2/orders/42
```

**Pros:** trivially cacheable per version (different URL = different cache key), visible in every log line and every curl command, trivial to route at the load balancer/gateway level to entirely different backend deployments.

**Cons:** the URI is supposed to identify a *resource*, and `/v1/orders/42` and `/v2/orders/42` arguably identify the same order — versioning the path conflates "which resource" with "which representation of the contract." REST purists consider this a layering violation. In practice it's the dominant approach anyway (Stripe, GitHub, Twilio) because operational simplicity usually wins.

## 3. Header-Based Versioning

The URL stays stable; the version travels in a request header.

```http
GET /orders/42
Accept: application/vnd.myapi.v2+json
```

or a custom header:

```http
GET /orders/42
Api-Version: 2026-03-01
```

**Pros:** keeps the URI a pure resource identifier; the `Accept` header form uses **content negotiation**, which is what HTTP designed headers for in the first place.

**Cons:** invisible to a quick glance at logs or a browser address bar; harder to test ad hoc (`curl` needs an extra flag); intermediary caches and proxies key on URL by default, so header-based variance needs `Vary: Api-Version` to cache correctly at all.

## 4. Query Parameter Versioning

```
GET /orders/42?version=2
```

Rare as the *primary* mechanism (it's easy to omit accidentally, and mixes control metadata into the same namespace as filters), but common as a fallback or for quick manual testing alongside another scheme.

## 5. Date-Based Versioning

Instead of integers, the version is the date the contract was frozen — Stripe's approach:

```http
GET /v1/charges/ch_123
Stripe-Version: 2024-06-20
```

Every account has a **pinned default version** (usually the date it was created), and it only moves forward when the integrator explicitly opts in by requesting a newer date. Internally the server keeps a table of transformation functions between consecutive dated versions and pipes an old request through however many transforms are needed to reach current internal logic, then pipes the response back down.

```
request (pinned: 2023-01-01) -> [transform 2023-01-01 -> 2023-06-01] -> [transform ... -> current] -> internal handler
response (current shape)     -> [transform current -> ... ] -> [transform ... -> 2023-01-01 shape] -> client
```

This scales far better than maintaining N parallel full implementations — there's one current implementation and a chain of small, independently testable shape-adapters.

![A dated request passes through a chain of transform functions to reach the current internal handler, then the response passes back through the inverse chain](diagrams/01-version-transform-chain.svg)

## 6. Semantic Field-Level Versioning

Instead of versioning the whole API, version individual capabilities via feature flags or field opt-ins:

```http
GET /orders/42
X-Enable-Features: new-pricing-model,extended-status
```

This avoids the combinatorial explosion of "v1 vs v2 vs v3 of the entire surface" when only one field actually changed, but it's harder to reason about which combination of flags a given client is actually running with, and testing the full cross-product of flags is often skipped in practice.

## 7. What to Version: The Whole API vs. a Single Resource

Most real systems don't cleanly bump "the whole API" at once — a single breaking change to one resource doesn't require every other resource to also gain a `/v2/`. Two structures:

```
/v1/orders, /v1/customers, /v1/products      (global version — simple, coarse)
/v2/orders, /v1/customers, /v1/products      (per-resource — only orders changed)
```

Per-resource versioning avoids forcing clients who only touch `/customers` to migrate when `/orders` changes, but it means "v1" and "v2" are no longer single coherent labels — the docs must state, per resource, which version is current.

## 8. Deprecation Policy

A version bump is only half the story — the old version has to eventually go away, on terms clients can plan around.

```http
GET /v1/orders/42 HTTP/1.1

HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/docs/migrating-to-v2>; rel="deprecation"
```

- `Deprecation` (a draft RFC header, widely adopted) marks a response as coming from a deprecated endpoint.
- `Sunset` (RFC 8594) gives the exact date the endpoint stops responding entirely.
- A documented **minimum notice period** (commonly 6–12 months for public APIs) is a trust commitment, not just a courtesy — integrators build migration schedules around it.

A responsible sunset also **actively measures usage of the old version** before killing it — deprecation headers are easy to ignore, and traffic dashboards per version are the only reliable signal that migration has actually happened.

## 9. Under the Hood

### Header-based versioning and the HTTP cache

Public/shared HTTP caches (CDNs, corporate proxies) key cache entries primarily by URL. Two requests to the identical URL `/orders/42` with different `Api-Version` headers look, to a cache that only inspects the URL, like the *same* cacheable resource — so a cache will happily serve a v1 response to a v2 request (or vice versa) unless told otherwise. The fix is the `Vary` response header:

```http
HTTP/1.1 200 OK
Vary: Api-Version
Cache-Control: max-age=60
```

`Vary: Api-Version` tells any RFC-compliant cache "store a separate cached entry per distinct value of this request header." Without it, header-based versioning is silently broken behind any cache that predates your versioning header being invented — which is most of them, because `Vary` support for arbitrary custom headers is inconsistent across CDNs, and some ignore non-standard `Vary` values entirely. This is a real operational reason URI-path versioning remains popular: `/v1/...` vs `/v2/...` are different URLs, so they cache correctly *for free*, on every cache that has ever existed, with zero configuration.

### The transform-chain pattern in detail

Maintaining N fully separate codebases for N versions means every bug fix and every new feature has to be back-ported N times, and N implementations drift out of sync with each other regardless of process discipline. The transform-chain approach (Stripe's actual documented architecture) keeps exactly **one** current implementation and models every past version as a pure, small, testable function from "one version's shape" to "the next version's shape":

```ts
type Transform = (payload: unknown) => unknown;

const transforms: Record<string, Transform> = {
  "2023-01-01": (p: any) => ({ ...p, status: p.state }),        // renamed state -> status later
  "2023-06-01": (p: any) => ({ ...p, amount_cents: p.amount }), // amount -> amount_cents later
};
```

A request pinned to `2023-01-01` runs the *inverse* of `"2023-06-01"`'s transform on the current handler's response (current -> 2023-06-01 shape -> 2023-01-01 shape), while a request already on `2023-06-01` skips straight to that step. Each transform is independently unit-testable in isolation, and the *number* of transforms in the codebase grows with the number of breaking changes ever made — not with the number of active client versions, because clients on the same version share the same chain segment. The real cost is that this chain only ever grows; retiring old transform steps requires actually sunsetting the versions that need them, which is exactly why deprecation policy (section 8) is not optional bureaucracy — it's what keeps the transform chain from becoming unbounded.

### Why "just add a field" isn't always safe

Even purely additive changes can break a client that does *strict* deserialization. A client using a schema validator in `additionalProperties: false` mode, or a statically typed language deserializing into an exact struct with no `#[serde(deny_unknown_fields)]`-style opt-out, will reject or drop data the moment the server adds an unlisted field. This is why API contracts (see the API Documentation topic) should explicitly state "clients MUST ignore unknown fields" as a rule of the contract itself — it converts what would otherwise be a breaking change (from the client's accidental strictness) into a genuinely safe one, but only for clients that were actually built to that rule.

## 10. Common Pitfalls

1. **No versioning strategy decided before the first breaking change is needed.** Retrofitting a scheme onto a live API with existing integrators is far more disruptive than picking one (even url path, the simplest) on day one.
2. **Bumping the major version for non-breaking changes.** This trains integrators to fear version bumps and ignore changelogs, because "v2" stops reliably signaling "you must act."
3. **Never actually removing old versions.** A "deprecated" endpoint with no enforced sunset date accumulates forever, multiplying the surface area every future change must consider.
4. **Versioning too granularly (per-field flags) without a discovery mechanism.** If clients can't query which flags exist and what they do, the feature-flag approach in section 6 becomes an undocumented maze.
5. **Silently changing behavior at the *same* version/URL.** If `Api-Version: 2` behavior is allowed to drift over time without a new version, "v2" stops being a stable contract at all — the version identifier only means something if it's frozen once published.
6. **Forgetting `Vary` with header-based versioning.** As covered in Under the Hood, this causes cache poisoning across versions that is very hard to diagnose because it appears intermittent (depends on cache state).
7. **No default/pinned version for new integrators.** If omitting the version header silently means "whatever is newest today," a client's behavior can change out from under it the next time the API adds a version, with no code change on their end at all.

## 11. Quick Reference

```
URI path       /v1/orders          simple, cacheable, visible; conflates resource + contract
Header         Accept: application/vnd.api.v2+json    clean REST; needs Vary; less visible
Query param    ?version=2          rare as primary; easy to omit
Date-based     Api-Version: 2026-03-01   Stripe-style; needs a transform chain

Classify first  additive/optional -> safe | removed/renamed/retyped/tightened -> breaking
Deprecation     Deprecation: true
                Sunset: <RFC 1123 date>
                Link: <...>; rel="deprecation"

Cache safety    Vary: Api-Version   REQUIRED for header-based versioning behind any cache
Contract rule   "clients MUST ignore unknown fields" turns additive changes into safe ones
Transform chain one current impl + N small pure functions between consecutive versions
```
