A breaking change shipped without warning burns integration trust in one deploy. API versioning solves that problem: evolve the API while every existing client keeps running in production. Three decisions shape the strategy: where the version lives (URI, header, or parameter), how CI catches breaks before merge, and how you retire an old version without drama.
What deserves a new version?
Additive changes break no contract. A new optional response field, an extra endpoint, a parameter with a default: all of that ships without a bump. A new version enters when the contract changes: renaming a field, removing a property, switching a type from string to integer, tightening validation. Version the contract; release numbers are marketing.
Why does URL path versioning (/v1/, /v2/) stay the REST default?
Path versioning wins on pragmatics. The version shows up in access logs, curl commands, and browser tabs; the router sends /v1/orders and /v2/orders to separate handlers with zero middleware; CDNs treat both as distinct cache entries. The flaw is conceptual: /v2/ implies the whole API turned over when the change touched a single resource. Twilio pushes the idea furthest with dates right in the path (/2010-04-01/) and has kept integrations alive for over a decade.
Header versioning via Accept
In the header model, clients send Accept: application/vnd.myapi.v2+json and the server routes on the media type. URLs stay clean and complexity stays hidden from casual consumers. The cost surfaces in operations: debugging requires inspecting headers, the CDN cache key must list the header (Vary control in CloudFront or Fastly), and support cross-references headers to learn which version served each request.
Contract first, code second
An OpenAPI spec as the single source of truth changes team dynamics. CI runs oasdiff against the previous spec and fails the pipeline when it detects a breaking change without a version bump. SDKs generated from the spec stay synchronized with the server implementation, and the same document feeds public docs. A contract under git becomes an auditable history of decisions.
How do you retire a version without losing clients?
- A Sunset header (RFC 8594) carrying the shutdown date on every old-version response
- Deprecation warnings inside the payload during the transition window
- A public changelog plus direct email to each integration owner
- A floor of 6 months notice for B2B and 12 months for enterprise contracts
Traffic decides the shutdown. Per-client and per-version usage logs show who still calls /v1/; the sunset fires once telemetry reports zero calls. Without those logs you flip the switch on schedule and find out Monday morning that your biggest customer still integrated.
Enjoyed this content?
I build web products and AI solutions the right way — solid architecture, maintainable code, and real delivery.
Let's talk