API Versioning: How Do You Change Your API Without Breaking Every Client?
You ship an API. Fifty people build on top of it. Then you need to change something — rename a field, restructure a response — and every one of those fifty integrations can break at once.
This is the question that shows up on every dev forum eventually: URI versioning or header versioning? Query params? Underneath that question is a simpler one: how do you change your API without waking up to fifty angry emails?
Here’s what actually works.
Put the version in the URL
/v1/users, /v2/users. Not a header, not a query string.
Header versioning looks cleaner on paper (Accept: application/vnd.company.v2+json), but clients forget to send it, it's harder to test without extra tooling, and it's harder to route in code. Query params (?version=2) are simple but look sloppy and aren't really RESTful.
URI versioning has one real downside: slightly longer URLs. In exchange you get something obvious to the client, easy to route on the server, and simple to test straight from a browser or curl.
Run both versions side by side
This is the part people skip. You don’t cut over — you run v1 and v2 at the same time for a while. Old clients keep hitting /v1 and nothing changes for them. New clients, or ones that have migrated, hit /v2.
A year is a reasonable window. Long enough for even a slow-moving integration partner to move on their own schedule, short enough that you’re not maintaining three live versions forever.
Get Bitroot’s stories in your inbox
Join Medium for free to get updates from this writer.
Only add. Never remove.
The rule that keeps both versions cheap to run: changes inside a version are additive. Add a phone field to the response and clients on the old integration just ignore it, since they were never asking for it. Don't rename email to email_address inside a version that's already live. That's a new version, not a patch.
Tell people the sunset is coming, more than once
Don’t just note “v1 deprecated” in a changelog nobody reads. Put deprecation headers on the old version’s actual responses:
Deprecation: true
Sunset: Sun, 31 Dec 2026 23:59:59 GMT
Link: </v2/users/123>; rel="successor-version"Then email the people using it directly. Once around month 6. Once more at month 11, as a final warning. By month 12, anyone still on the old version has had two direct heads-ups plus months of a header telling their own tooling the same thing.
The mistake that undoes all of this
Teams get the versioning part right, then quietly remove a field from a live version anyway, because “barely anyone uses it.” That one field is enough. Somewhere, one integration reads it, and it goes down — and the version number in the URL doesn’t save you, because the payload underneath changed shape without warning.
Versioning without holding the additive-only rule inside each version is just signage. It looks like a system, but it isn’t one.
We wrote up the full setup, including the Express.js route structure, the deprecation headers, and contract tests that catch this exact mistake before it ships: bitroot.org/guides/api-versioning-that
If you’re building an API that other people’s businesses run on, this isn’t something you get to later. It’s the difference between a deprecation nobody notices and a support inbox that won’t stop.
Full step-by-step, including the route setup and the monitoring code for tracking who’s still on the old version: bitroot.org/guides/api-versioning-that — free, no paywall.




