Quick take
Your API is a contract. Version it with URL paths. Define “breaking” before you ship v1. Deprecate on a timeline, not a whim. Support two versions max or you’ll drown.
APIs break. That’s the starting point.
I’ve been dealing with this at the fintech startup. We expose financial data to partners, and the moment someone integrates your API, you own their uptime. Adding a field? Fine. Renaming one? You just broke someone’s production pipeline at 2 AM.
Versioning isn’t some academic exercise. It’s damage control. You’re going to change things – the question is whether your consumers find out from your docs or from their error logs.
The four options (and what I’d actually pick)
URL path versioning. This is what we went with at the fintech startup. Dead simple. You see the version in every request, every log line, every curl command someone pastes into Slack. Routing is trivial. Caching works out of the box. The tradeoff is that upgrading means changing URLs, but honestly? That’s a feature, not a bug. It forces consumers to consciously opt in.
GET /v1/users/123
GET /v2/users/123
Query parameter versioning. Keeps URLs stable and you can default to the latest version if the param is missing. Sounds nice until you realize half your consumers will forget to pin the version, and then your “non-breaking” release breaks them anyway. Caching also gets weird.
GET /users/123?version=1
GET /users/123?version=2
Header versioning. Clean URLs, plays well with content negotiation. But nobody can see it. Debugging becomes a guessing game. Proxies strip custom headers. Your support team will hate you.
GET /users/123
Accept: application/vnd.myapi.v1+json
No versioning at all. Works if your API is tiny and you have iron discipline about backward compatibility. We considered this for some internal endpoints at the fintech startup. Rejected it. The surface area grows faster than you think, and one day you need to make a change that simply can’t be backward compatible. Then what?
My recommendation: URL path versioning. It’s boring. It works. You’ll thank yourself when you’re debugging a production issue at midnight and the version is right there in the URL.
Know what “breaking” actually means
This sounds obvious but most teams get it wrong. They ship something, a consumer complains, and then there’s a debate about whether it was breaking.
Define it upfront. These are breaking: removing a field, removing an endpoint, changing a field’s type, making an optional parameter required, changing error response formats, changing auth rules, renaming resource identifiers. Full stop.
These are safe: adding optional fields, adding new endpoints, adding optional parameters, performance improvements, bug fixes. Existing clients can ignore all of these.
Then there’s the gray zone that will bite you. Changing what a field means without changing its type. Tightening rate limits. Adding required fields to creation endpoints. These can be fine for some consumers and catastrophic for others.
My rule: if there’s any doubt, it’s breaking. Version it. Being too cautious costs you almost nothing. Being too aggressive costs you a partner.
Deprecation is a process, not an email
At the fintech startup we learned this the hard way. You can’t just announce “v1 is going away” and expect people to move. Most won’t read the announcement. Some will read it and ignore it because they have other priorities.
Signal it in the responses themselves. Use sunset headers with actual dates.
Deprecation: true
Sunset: Sat, 31 Dec 2017 23:59:59 GMT
Link: </docs/migration>; rel="deprecation"
Run both versions in parallel. Track who’s still on the old version. Reach out to the heavy users directly – not a mass email, an actual conversation. When the sunset date hits, only kill what you announced. No surprises.
Implementation: keep it boring
You have two real choices. Separate handlers per version: easy to understand, annoying to maintain because you’re duplicating logic. Or a transformation layer: one set of core logic with response adapters per version. We went with the transformation approach at the fintech startup. It’s cleaner until the adapters start accumulating edge cases, so stay disciplined about cleaning them up.
Feature flags are tempting for small differences. They work until you have 30 flags and nobody remembers which ones are safe to remove. Use them sparingly.
Guardrails that actually help
Document your versioning policy before you ship v1. What’s breaking, how long deprecation windows last, how many versions you’ll support concurrently. Publish a migration guide with every new version – not just what changed, but why and how to update.
Most importantly: limit concurrent versions. “Current plus one” is the sweet spot. Supporting every old version sounds generous but it’s a tax on every feature you build going forward. At the fintech startup we committed to supporting two versions. It forces the ecosystem forward and keeps our maintenance burden sane.
Version early. Version explicitly. And don’t let old versions linger out of politeness – that’s how you end up maintaining five parallel APIs and shipping nothing new.