The day your API gets its first external consumer, you lose the freedom to change it casually. Renaming a field that a mobile app depends on breaks that app for every user until they update. Versioning is how APIs evolve without betraying existing clients. In this lesson we define what actually breaks clients, compare the three main versioning strategies, and lay out a humane deprecation process.
Additive changes are safe: new endpoints, new optional request fields, new fields in responses (clients must ignore unknown fields, a rule worth documenting). Breaking changes include removing or renaming a field or endpoint, changing a field's type or format, tightening validation on existing inputs, changing error codes or status codes clients rely on, and changing authentication requirements. The whole point of versioning is to ship the second list without disturbing anyone still on the old contract.
The version lives in the path: /api/v1/products versus /api/v2/products.
GET https://api.shopcraft.com/v1/products/42 -> {"name": "...", "price": 79.99}
GET https://api.shopcraft.com/v2/products/42 -> {"name": "...", "price": {"amountCents": 7999, "currency": "USD"}}This is the most common approach (GitHub, Twitter historically, countless internal APIs) because it is unmissable: the version is visible in every log line, every browser test, every copied curl command. Routing v1 and v2 to different code paths or even different services is trivial. Purists object that /v1/products/42 and /v2/products/42 are the same resource with two URLs, but the operational clarity usually wins.
The URL stays clean and the version travels in a header, either custom (X-API-Version: 2) or via content negotiation with a vendor media type: Accept: application/vnd.shopcraft.v2+json. GitHub's modern API uses X-GitHub-Api-Version: 2022-11-28. This is the more RESTful design and keeps URLs permanent, at the cost of discoverability: versions are invisible in logs and URLs, harder to test in a browser, and easier to forget entirely, which means you must define what happens when the header is absent (usually: default to the oldest supported version, never the newest).
Stripe's famous approach: each account is pinned to the API version (a date like 2026-03-15) that was current when it first integrated, and stays there until it explicitly upgrades or overrides per-request with a Stripe-Version header. The provider maintains transformation layers between adjacent versions. This is the gold standard for developer experience and the most engineering-intensive to operate; it suits platforms whose entire business is their API.
For most teams the pragmatic answer is URL versioning with coarse major versions only: v1, v2, never v1.3 in the URL (minor, additive changes need no version at all). Create v2 only when forced by accumulated breaking changes; every live version multiplies your testing and support surface, and the goal is to run as few as possible, ideally one or two.
Retiring v1 is a process, not an announcement. Communicate a timeline (6 to 12 months for public APIs), emit warnings in responses via the Deprecation and Sunset headers so tooling can detect them, monitor traffic to identify remaining v1 callers and contact the noisy ones directly, then brown-out (serve temporary errors for short windows) before the final shutdown with 410 Gone. Every step exists because someone, somewhere, has not read your changelog.
Next up: Error Handling and Validation, making failure a well-designed part of your contract.