Writing / 2017
API Versioning Strategies: What Works and What Doesn't
URL path, query, header, or no API versioning? What worked for a fintech partner API, what counts as breaking, and how to deprecate cleanly.
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 through an API designed for consistency , 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 is 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 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. I count that in its favor: it forces consumers to opt in consciously.
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. (GraphQL gets close to this by letting clients ask only for fields they know, with its own costs .) 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, and then you’re versioning anyway, just without a plan.
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.
Define what “breaking” 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 needs a process
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. The IETF Sunset header is still a draft, but it’s simple and gives clients a machine-readable date.
Sunset: Sun, 31 Dec 2017 23:59:59 GMT
Run both versions in parallel. Track who’s still on the old version. Reach out to the heavy users directly. Skip the mass email and have 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: policy, migration guides, version limits
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 that covers what changed, 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.