Every API is a promise. The moment another team, a mobile app, or a paying customer writes code against your endpoints, you have signed a contract you cannot unilaterally tear up. Breaking that contract is one of the fastest ways to lose trust, and it usually happens by accident—a field renamed here, a default changed there, a "small cleanup" that takes down someone's production integration overnight. In early 2025 the pressure on that contract has only intensified: AI coding assistants are generating client integrations at a pace nobody planned for, and the major model providers are shipping new API versions month after month, training a whole industry to expect that "v2" should never silently break "v1." As the CTO at Softechinfra, I have spent years designing and reviewing APIs across our web development projects, and the teams that ship calmly are not the ones with the cleverest versioning scheme. They are the ones who decided early what counts as a breaking change, picked one strategy, and wrote down a deprecation policy before they needed it. This guide is that playbook.
What Actually Counts as a Breaking Change
Before you choose a versioning strategy, you have to agree on what versioning is protecting against. A breaking change is any change that can make a previously valid client request fail or behave differently. Most teams underestimate how many everyday edits qualify.
Here is the line we draw on every project. These are breaking, and require a version bump or a careful migration:
- Removing or renaming a field, endpoint, or parameter
- Changing a field's data type (string to number, scalar to array)
- Making a previously optional request field required
- Tightening validation so inputs that used to pass now fail
- Changing the meaning of an existing field or its default value
- Altering error codes, status codes, or pagination behavior clients rely on
And these are non-breaking—ship them freely without a new version:
- Adding a new endpoint or a new optional request parameter
- Adding a new field to a response (clients should ignore unknown fields)
- Adding a new optional value to an enum, documented as expandable
- Relaxing a validation rule that used to reject valid-looking input
That last category is the secret to slowing down how often you need to version at all. The single most valuable contract you can publish is "clients must tolerate unknown fields and ignore them." Once consumers agree to that, an enormous class of additions stops being breaking. We treat this as a foundational principle alongside the rest of our API design best practices.
The Three Versioning Strategies
There are realistically three places to put a version: in the URL path, in a header, or nowhere visible at all (the version is a date, negotiated out of band). Each is a legitimate choice with a different center of gravity.
| Strategy | Example | Best For |
|---|---|---|
| URI / path versioning | GET /v2/orders |
Public APIs, easy debugging, broad client base |
| Header versioning | Accept: application/vnd.api+json; version=2 |
Purist REST, clean URLs, sophisticated clients |
| Date / semantic versioning | API-Version: 2025-02-14 |
Frequently evolving APIs, gradual per-client upgrades |
URI versioning is the most common and the most pragmatic. The version is visible in the path, so it shows up in logs, browser address bars, curl commands, and bug reports. It is trivially cacheable and impossible to get wrong by accident. Purists object that the URL should identify a resource, not a representation, and they are technically correct—but for most teams, especially those exposing an API to outside developers, the operational clarity wins. This is what we reach for first.
Header versioning keeps URLs clean and treats the version as part of content negotiation, which is arguably more RESTful. The cost is real: a version buried in a header is easy to forget, harder to debug, and invisible to anyone reading an access log. It works well when your clients are sophisticated and your tooling makes the header impossible to omit.
Date-based versioning—popularized by Stripe and adopted by several major API providers—pins each client to the API as it existed on a given date. New clients get the latest behavior; existing clients keep their pinned date until they explicitly upgrade. Behind the scenes, the server runs request and response transformations to translate between the pinned version and the current one. It is the most powerful model for an API that changes constantly, and also the most engineering-intensive to build and maintain.
A practical note on numbering: reserve major version bumps (v1 to v2) for genuinely breaking changes only. Do not increment the version for every feature. A new major version is expensive—you now maintain two surfaces—so it should mean "we broke the contract on purpose," not "we shipped something." If you are weighing REST against alternatives at this stage, our GraphQL vs REST comparison covers how schema evolution differs between the two; GraphQL's field-deprecation model sidesteps some, but not all, of these problems.
Backward Compatibility Rules That Hold as You Grow
Versioning is the escape hatch. Backward compatibility is the discipline that means you rarely need it. The teams that version least are the ones who design responses to be additive from day one.
Tolerant readers
Clients ignore unknown fields and never fail on additions. Document this as a contract so adding response fields stays non-breaking forever.
Expand, then contract
To change a field, add the new one alongside the old, migrate clients, then remove the old one in a later version. Never rename in place.
Stable defaults
Treat default values as part of the contract. Changing a default silently shifts behavior for every client that relied on it—that is a breaking change in disguise.
Explicit nullability
Decide whether a field can be null, document it, and never quietly flip it. Clients write parsing logic around your nullability promises.
The expand-then-contract pattern deserves emphasis because it is how you make a "breaking" change without breaking anyone. Say you want to replace a single name field with firstName and lastName. You do not delete name. You add the two new fields, populate all three for a transition period, announce the deprecation of name, give clients a real window to migrate, and only then remove it—in the next major version. The change ships in slow motion, on purpose, and nobody's integration falls over on a Tuesday.
This is also where an API gateway earns its keep. Routing, version translation, and deprecation headers all live cleanly at the gateway layer, so your core services stay focused on business logic instead of compatibility shims. Pairing versioning discipline with an API-first development approach—designing the contract before the implementation—catches most breaking changes in review, before a single client ever sees them.
A Deprecation Policy You Write Before You Need It
The worst time to invent a deprecation policy is the day you want to delete something. Write it once, publish it, and apply it the same way every time. Ours has five steps.
- Announce in advance. Publish the deprecation in your changelog and developer docs the moment the decision is made, with the replacement clearly named and a migration example.
- Signal in the response. Send a
Deprecationheader (and aSunsetheader with the removal date) on every call to a deprecated endpoint, so client teams see it even if they never read the changelog. - Give a real window. Define a minimum support window—six to twelve months is reasonable for a public API—and never shorten it under pressure.
- Measure who is still on it. Track usage of the deprecated surface. You cannot turn something off responsibly if you do not know who is still calling it.
- Remove, then communicate again. When usage approaches zero and the window has passed, remove it in a new major version and announce the removal one more time.
We learned to take this seriously on real products. On ExamReady, the exam-preparation platform we built, the mobile and web clients consume the same API but ship on different release cadences—the web app updates the day we deploy, while app-store review delays mean a meaningful share of users run an older mobile build for weeks. A field we "cleaned up" on the server can vanish from under a phone that has not updated. The deprecation window is not bureaucracy there; it is the only thing standing between a backend refactor and a one-star review. The same logic applies to any product with a long client tail, including the integrations behind our in-house English-speaking app TalkDrill.
Putting It Together
If you are starting an API today, the durable defaults are simple. Pick URI versioning unless you have a specific reason not to. Commit to tolerant readers and additive changes so you almost never need a new version. Use expand-then-contract for anything that looks breaking. And write the deprecation policy now, while nothing is on fire, so that when you finally do retire v1 it is a calm, scheduled, well-signaled event rather than an outage with your company's name on it.
None of this is exotic. It is the same boring discipline that lets a small team support thousands of integrations without a versioning war room—the kind of architectural call our founder Vivek Singh and I make at the start of every build, because the cost of getting it wrong compounds for years. The model APIs that everyone is racing to integrate in early 2025 will be replaced by newer ones soon enough. The rules that keep their clients from breaking will not change at all.
Building or Scaling an API That Outside Teams Depend On?
We design versioning strategies, deprecation policies, and backward-compatible API contracts that grow with your product instead of breaking your clients.
Talk to Our Engineering Team →
