API Versioning Strategy — Is There a Right Answer?
URL path, header, query param... what I learned versioning APIs the hard way
The Client Broke on a Friday Afternoon
Friday, 4 PM. Slack message from the mobile team: "Looks like the API response structure changed — app is crashing." It was because of a change I deployed that morning. I split the name field in the user object into firstName and lastName. The mobile app was still reading name.
The cost of changing an API without versioning. Hotfix: re-add name, mark it deprecated, tell the mobile team "please migrate to the new fields within two weeks." After this incident, I started thinking seriously about API versioning.
URL Path: /v1/users
The most intuitive approach. /api/v1/users, /api/v2/users — version right in the URL. GitHub, Stripe, and Twitter APIs use this.
The advantage is clarity. The URL tells you the version instantly. Documentation is straightforward. But routing gets complex as versions multiply. Each version needs its own controllers, and sharing common logic while branching on differences turns code into spaghetti.
In my project, running v1 and v2 simultaneously meant fixing a bug in v1 required the same fix in v2. That dual maintenance was more exhausting than expected. Three months later, v1 traffic was 4% of total, but I couldn't shut it down — that 4% was one large client.
Header-Based: Accept-Version
Accept: application/vnd.api+json;version=2 — version in the request header. Clean URLs are the upside.
In practice, though, it was awkward. Testing the API directly in a browser requires setting headers. Postman and curl handle it fine, but team members kept typing URLs into the browser address bar and asking "why doesn't it work?"
API gateway configuration gets harder too. Most proxies support URL-based routing natively. Header-based routing requires extra setup.
Query Parameter: ?version=2
/api/users?version=2. Simple, easy to implement. But caching becomes an issue. CDNs cache by URL, and different query params mean separate cache entries. Misconfigure and v2 requests get cached v1 responses.
This exact thing happened to me. (For exactly 11 minutes, v2 requests returned v1 responses.) A user reported "data looks wrong" before monitoring caught it. Eleven minutes sounds short, but roughly 4,300 requests hit that broken cache.
What I Use Now
Ended up with URL path versioning. The reason is simple: it's what the most people understand. Telling API consumers (frontend, mobile, external partners) "use v2" is way easier than "put this in the header."
But I enforce strict versioning policies. Adding fields is non-breaking — no version bump. Removing fields or changing types always means a new version. Previous versions stay alive for at least 6 months after a new version deploys. These rules live on page one of the API docs.
If You Ask Whether There's a Right Answer
There isn't. It depends on team context, who consumes the API, and how often it changes. But one thing is certain: "no versioning" is not an answer. The lesson from that Friday afternoon is still vivid.
Once an API is public, you can't change it freely. Accept that at the design stage. I learned this after the incident.