caduh

API Versioning Strategies — pick one and stick to it

4 min read

What counts as a breaking change, URL vs header versions, deprecation signals, and how to evolve APIs without breaking clients. With routing and policy snippets you can paste today.

TL;DR

  • Version only for breaking changes. Additive changes (new optional fields/endpoints) should be backward‑compatible.
  • Prefer path versions (/v1) for public APIs—clear, cache‑friendly, and easy to route. Use media types/headers when you need finer control.
  • Communicate and phase changes: ship deprecations, send Sunset/Deprecation headers, run versions in parallel, and provide a migration guide.

1) What is a breaking change? (decide this first)

  • Remove or rename fields/endpoints.
  • Change types/semantics (string → number, units, enum values).
  • Tighten validation in a way that rejects previously valid requests.
  • Change defaults (pagination/sort) that alter results for existing clients.
  • Rename error codes or envelopes in a way clients can’t parse.

Non‑breaking (usually): adding optional fields, adding new endpoints, widening enum values, adding new error codes while keeping the same error shape.


2) Versioning strategies (with pros/cons)

A) Path version (recommended for most external APIs)

GET /v1/orders/123
GET /v2/orders/123

Pros: obvious to humans and proxies; trivial routing and docs; cache‑friendly.
Cons: URL churn; multiple docs to maintain.

B) Media type / header version

GET /orders/123
Accept: application/vnd.example.orders+json;version=2

Pros: fine‑grained, can version resources independently; no URL change.
Cons: more complex to cache/proxy; clients must remember headers; doc tooling harder.

C) Query param (use sparingly)

GET /orders/123?version=2

Works, but hurts caches and can leak ACLs/SEO.

D) Subdomain (niche)

GET https://v2.api.example.com/orders/123

Operationally heavier; okay for “big bang” eras.

Pick exactly one public strategy and enforce it consistently.


3) Deprecation & migration signals

  • Headers
    • Deprecation: true
    • Sunset: <HTTP-date> (when the old version stops)
    • Link: <https://docs.example.com/migrate-v1-to-v2>; rel="deprecation"
  • Docs: changelog + migration guide + code examples.
  • Phasing: deprecate → warn → freeze features → security‑only → sunset.
  • Monitoring: identify active clients on old versions and contact them.

4) Compatibility rules that keep you out of trouble

  • Treat your error envelope as part of the public contract; add new codes but keep the shape stable.
  • Prefer objects to positional arrays so you can add fields safely.
  • Use explicit nullable/optional semantics; don’t repurpose fields.
  • For pagination, prefer cursor/keyset so adding rows doesn’t reshuffle pages.
  • Use feature flags on the server to run v1/v2 side by side while you validate.

5) Routing & policy snippets

Nginx (edge split for /v1 vs /v2)

server {
  listen 443 ssl http2;
  server_name api.example.com;

  location ~ ^/v1/ { proxy_pass http://api-v1; }
  location ~ ^/v2/ { proxy_pass http://api-v2; }
}

Envoy (header‑based versioning)

- match:
    prefix: /orders
    headers:
      - name: accept
        safe_regex_match: { regex: ".*version=2.*" }
  route: { cluster: orders-v2 }
- match: { prefix: /orders }
  route: { cluster: orders-v1 }

AWS API Gateway (two stages)

Routes:
  - RouteKey: "ANY /v1/{proxy+}"
    Target: "integrations/V1"
  - RouteKey: "ANY /v2/{proxy+}"
    Target: "integrations/V2"

Kong (path + deprecation header plugin sketch)

services:
  - name: orders-v1
    url: http://orders-v1:8080
    routes: [{ paths: ["/v1"] }]
    plugins:
      - name: response-transformer
        config: { add: { headers: ["Deprecation:true", "Sunset: Sun, 01 Dec 2025 00:00:00 GMT"] } }
  - name: orders-v2
    url: http://orders-v2:8080
    routes: [{ paths: ["/v2"] }]

6) GraphQL vs REST (quick guidance)

  • REST: Use /vN or media types. Additive changes are non‑breaking if your envelope stays stable.
  • GraphQL: Prefer never breaking—add new fields/types; mark old fields with @deprecated; don’t remove until usage is zero. Version the schema (docs) rather than the endpoint.

7) SDKs & clients

  • Generate clients from OpenAPI/SDL; pin major versions.
  • Add runtime capability checks (e.g., feature discovery endpoints) when feasible.
  • Emit warnings when an SDK is used against a deprecated API version.

8) Pitfalls & fast fixes

| Pitfall | Why it hurts | Fix | |---|---|---| | Versioning every tiny change | Version sprawl | Version only breaking changes | | Silent breaking change | Client outages | Phase with headers + migration guides | | Multiple public styles | Confusion | Pick one strategy and document it | | No coexistence period | Downtime | Run v1 and v2 in parallel at the edge | | Changing error envelope | Parser failures | Keep the shape; add new codes only | | “Big bang” rewrites | Risk & delays | Ship small, additive steps first |


Quick checklist

  • [ ] Declare what counts as breaking for your API.
  • [ ] Choose path or header versioning and standardize it.
  • [ ] Send Deprecation/Sunset headers and link migration docs.
  • [ ] Keep error envelope stable; add fields additively.
  • [ ] Run versions in parallel; monitor old clients.
  • [ ] Provide SDKs and test sandboxes for new versions.

One‑minute adoption plan

  1. Publish a short versioning policy (breaking vs additive).
  2. Implement routing for /v1 and /v2 at the edge.
  3. Add Deprecation/Sunset headers to v1 and a migration guide link.
  4. Keep v1 and v2 live together; monitor usage, reach out to stragglers.
  5. After the sunset date, retire v1 and clean up docs & SDKs.