DEV Community

Manoj Kumar Kagitha
Manoj Kumar Kagitha

Posted on

Every API Breaks Eventually. Versioning Decides Who Feels It.

If you have run APIs at enterprise scale, you know the call. A partner's app starts throwing 400s at noon on a Tuesday. Someone renamed a field in a response payload. Nobody versioned anything, so every client got the change at once. The fix takes ten minutes. The trust takes months to rebuild.

Breaking changes are normal. Software grows. Fields get renamed, auth schemes get tightened, pagination shows up where it did not exist before. What is not normal is shipping a breaking change without a version. Versioning decides who feels a breaking change: nobody, or everyone.

Here is how we run API versioning across a multi-gateway estate. I work with Apigee X and Azure APIM side by side, plus backing services on Azure. The estate is the hard part. One versioning policy only works if it holds on every gateway at once.

Pick one versioning scheme and mean it

There are three schemes people argue about: path versioning (/v1/orders), header versioning (Accept: application/vnd.orders.v2+json), and query params (?version=2).

Path versioning wins in practice. Here is why. Every client knows how to hit a different path. Every log aggregator groups by path without custom parsing. Every gateway routes on path without policy gymnastics. Headers get stripped by proxies you do not control. Query params get swallowed by caches. Paths survive.

The rule is simple: the version lives in the path, it is mandatory, and it never changes meaning. /v1 stays /v1 forever. If v1 needs a fix, you ship /v2 and deprecate v1. You never mutate v1 under the same URL.

Put the policy in the gateway, not in every backend team

The biggest versioning failure I have seen was not a bad scheme. It was a good scheme enforced nowhere. Teams were told to version their APIs. Half did it their way, half forgot, and one shipped /v1.2-final-2.

The gateway is the enforcement point. It sits in front of every service. That is where the versioning policy lives, written once, applied to every API that crosses it.

A practical gateway policy does three things:

  1. Rejects requests with unknown or missing versions with a clear 400.
  2. Routes each version to the right backend. /v1/orders and /v2/orders can point at the same service or different ones. The client never knows.
  3. Normalizes version metadata for logging, so analytics can answer "how much traffic is still on v1" without a forensic exercise.

In Azure APIM this is an inbound policy on the product or gateway scope. Something like this:

<policies>
  <inbound>
    <base />
    <choose>
      <when condition="@(context.Request.OriginalUrl.Path.StartsWith("/v1")
          || context.Request.OriginalUrl.Path.StartsWith("/v2"))">
        <set-header name="x-api-version" exists-action="override">
          <value>@(context.Request.OriginalUrl.Path.Substring(1, 2))</value>
        </set-header>
      </when>
      <otherwise>
        <return-response>
          <set-status code="400" reason="Bad Request" />
          <set-body>{"error": "unknown API version. Use /v1 or /v2."}</set-body>
        </return-response>
      </otherwise>
    </choose>
  </inbound>
</policies>
Enter fullscreen mode Exit fullscreen mode

The same logic exists in Apigee as a set of conditional flows and a RaiseFault policy. Different syntax, same behavior. That is the point. The behavior is one policy, expressed twice. We keep a short spec doc for "version enforcement behavior" and review both gateway implementations against it whenever either changes.

Versions need a registry, not just a URL pattern

A URL pattern tells clients where to go. It does not tell anyone what is live, what is deprecated, or who still calls the old version. For that you need a registry: every API, every active version, the owner team, the deprecation date, and the known consumers.

This sounds like paperwork. It is the thing that makes deprecation possible. Without a consumer list, deprecating v1 means guessing. With one, it means sending each consumer a date and a migration guide, then watching the traffic shift on the dashboard.

The registry does not need to be fancy. A structured API catalog backed by the same onboarding flow teams already use works. The important part is that publishing a new version and updating the registry are the same action, not two separate chores that drift apart.

Deprecation is a schedule, not an announcement

Announcing a deprecation and executing one are different jobs. Announcements get read and forgotten. Schedules get followed. A deprecation schedule has four parts:

  • A published end-of-life date, at least 90 days out for internal consumers, longer for partners. Nobody gets surprised.
  • Sunset headers on responses from the deprecated version, so clients see the date every time they call. Sunset and Deprecation headers exist for exactly this.
  • Staged traffic reduction: monitor v1 traffic weekly, reach out to the stragglers directly. The last 5 percent of traffic is always the hardest and always belongs to the team you least expect.
  • A hard cutoff, enforced at the gateway, on the published date. The gateway returns 410 Gone with a link to the migration guide. Soft cutoffs become permanent.

This is where the gateway policy pays for itself a second time. The cutoff is a one-line change in the routing policy, not a coordinated deploy across backend teams.

Validate the contract, not just the shape

Versioning keeps clients on the contract they coded against. Contract validation keeps the contract honest. At the gateway, that means schema validation on requests and responses where it is cheap enough to run.

The practical version: JSON Schema validation on the inbound side for critical endpoints. It catches the "we added a required field without bumping the version" mistake before it ships. Pair it with contract tests in CI on the backend side. The gateway check is the backstop, not the test suite.

Keep validation scoped. Validating every payload on a high-throughput API adds latency you will feel at P99. Validate the versioned endpoints that carry breaking-change risk, and let observability cover the rest.

The short version

Breaking changes are inevitable. Surprises are optional. One versioning scheme, enforced at every gateway, tracked in a registry, deprecated on a schedule, with contract validation at the choke point. That is the whole playbook. It is unglamorous work. It is also the difference between a Tuesday afternoon incident call and a migration that finishes before anyone notices.

Top comments (0)