Under the Hood
Apis

Versioning & evolving an API without breaking clients

The day your API has clients you don't control — a mobile app on a million phones, a third-party integration — you lose the freedom to change it. Old versions live on for months or years and you cannot force anyone to update, so the API has to evolve without breaking the clients already out there. This lesson separates the changes that are safe from the ones that brick old clients, lays out the versioning strategies (URI vs header) and why additive change beats new versions, and covers the mobile-specific reality that makes a deployed app the least forgiving API client there is.

Versioning & evolving an API without breaking clients

While an API has exactly one client that you deploy in lockstep with the server, versioning is a non-problem — change both at once and move on. Everything changes the moment there are clients you don't control on a release you can't recall: a mobile app sitting on users' phones, a partner's integration, a public API. Now the server and the client evolve independently, old versions persist in the wild, and a change that's trivial in a monorepo becomes a question of "will this break the app that shipped six months ago and is still on a third of our users' phones?" This lesson is about changing an API while keeping faith with the clients already out there — the discipline that separates an API you can evolve from one you're afraid to touch.

Two directions of compatibility

The goal is compatibility, and it has two directions worth naming because different changes threaten each:

  • Backward compatibility — a new server still works with an old client. This is the one that matters most for a deployed mobile app: you ship a new backend, and the app version from months ago must keep working against it.
  • Forward compatibility — an old server works with a new client, which mostly comes down to clients being built to ignore fields they don't understand rather than choke on them (a "tolerant reader").

Break backward compatibility and you break apps in the field that you cannot patch. That's the failure this whole lesson is organized to prevent.

Which changes are safe, and which brick clients

The core skill is knowing, for any change, whether it's additive (safe) or breaking. The rule of thumb: adding things is usually safe; removing, renaming, or changing the meaning of things is not.

Safe (additive) changes — an old client, which ignores what it doesn't know, keeps working:

  • Adding a new optional field to a response (old clients don't read it).
  • Adding a new endpoint.
  • Adding a new optional request parameter with a sensible default.
  • Relaxing a constraint (making a previously-required field optional).

Breaking changes — an old client that depended on the old shape now fails:

  • Removing or renaming a field (the old client reads amount, you renamed it to amount_paise, it now reads undefined).
  • Changing a field's type or units (a number becomes a string; rupees become paise — the value "500" now means something 100× different).
  • Making an optional field required, or adding a new required request parameter (old clients don't send it).
  • Changing semantics without changing the shape — the sneakiest, because the schema looks identical but the behavior differs (a status that used to include pending now excludes it).
  • Changing error codes or status the client branches on.

The units/type example is worth dwelling on because it's a silent break: nothing errors, the old client just displays or computes the wrong number. Those are the worst kind — a loud break gets caught; a silent one ships.

Click through the changes and watch which ones an old client survives. The dangerous ones aren't the loud breaks — they're the silent ones (changing units or semantics) where nothing errors and the old client just quietly computes the wrong number.

v1 (old){ "amount": 500 }
v2 (new){ "amount": 500, "note": "dinner" }
✓ additive — safe · old client amount → 500Old clients ignore fields they don’t know about, so a new optional field changes nothing for them.

The rule: adding things is usually safe (old clients ignore what they don't know); removing, renaming, or changing the meaning of things breaks them. The ones to fear are the silent breaks — a units or semantics change where nothing errors and the old client just computes the wrong number. A loud break gets caught in testing; a silent one ships to production.

Versioning strategies

When you genuinely must make a breaking change, you version. The strategies:

  • URI versioning/v1/expenses, /v2/expenses. The version is right there in the path: explicit, trivially visible in logs, easy to route to different handlers, and dead simple to reason about. The most common choice in practice, and the pragmatic default. (Purists object that a resource shouldn't change identity just because its representation did — but the operational clarity usually wins.)
  • Header versioning — the client sends Accept: application/vnd.myapi.v2+json (or a custom header). URLs stay "clean" and a resource keeps one URI, which is the more RESTfully-pure model — at the cost of being invisible in a URL, harder to test with a plain browser, and easier to get wrong.
  • Query parameter?version=2. Simple but discouraged; it muddles caching and mixes versioning into the resource's query space.

But here's the thing every experienced API author knows: a new version is expensive — it means maintaining two (then three) parallel code paths, docs, and test suites indefinitely. So the real discipline is to avoid new versions by making changes additive, and reserve version bumps for genuine, unavoidable breaks. Alongside that:

  • Expand and contract (parallel change) for a necessary field change: add the new field alongside the old, populate both, migrate clients over time, and only remove the old one once telemetry says no live client uses it. Never a rename-in-place.
  • Deprecate loudly and slowly — announce, send Deprecation/Sunset headers, give a long window, and watch usage drop to zero before removing.

Go deeper

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. Explain why versioning is a non-problem when server and client deploy together, and what specifically changes once there are clients you do not control and cannot recall.
  2. Define backward vs forward compatibility, and say which one a deployed mobile app makes non-negotiable and why.
  3. Classify these as safe or breaking and justify each: adding an optional response field; renaming a field; making an optional field required; changing a money field from rupees to paise. Which kind of break is the most dangerous, and why?
  4. Contrast URI and header versioning on visibility, routing, and REST purity. Why do experienced authors treat a new version as a last resort, and what is expand-and-contract?
  5. Why is a deployed mobile app the least forgiving API client? Explain why you cannot simply roll back a breaking change the way you can for a web frontend.
  6. Fable is /v1 URI-versioned, mobile, and stores money as paise. Explain the additive-only rule it follows inside v1, why the money units make a careless change catastrophic, and what a minimum-supported-version gate buys that additive discipline alone cannot.