Table of contents
An API is a promise. The moment another team, a mobile app or a partner depends on it, every change you make is a change to their system too. Versioning is not really about numbers in a URL; it is about how you keep that promise while the product underneath keeps changing.
This guide covers what actually counts as a breaking change, the common versioning approaches and their trade-offs, how to evolve an API additively so most changes need no new version at all, and how to run deprecation so clients have time and information to move. It pairs with our [API gateway patterns guide](/blog/api-gateway-patterns-for-enterprise), which covers where versions are routed.
- Default approach
- Evolve additively; add a new version only for genuine breaking changes
- Breaking changes
- Removing or renaming fields, changing types or meaning, tightening validation
- Deprecation
- Announce early, signal in responses, set a date, and measure remaining usage
- Safety net
- Contract tests against real consumer expectations
What counts as a breaking change
A change is breaking if a well-behaved existing client stops working correctly without modifying its code. That is a wider category than people expect:
- Removing or renaming a field, endpoint or enum value.
- Changing a field's type, format or meaning (a price that was in rupees is now in paise).
- Making an optional input required, or tightening validation so previously valid requests are rejected.
- Changing default behaviour, ordering, pagination or error codes that clients may rely on.
- Adding a new enum value when clients assume the set is closed.
Adding a new optional field or a new endpoint is normally safe, provided clients are written to ignore what they do not recognise. That tolerance (sometimes called the robustness principle) is the foundation of additive evolution, and it is worth stating explicitly in your API documentation.
Prefer additive evolution
The cheapest version is the one you never have to create. If you can add rather than change, do: introduce a new field alongside the old one, keep both populated for a transition period, then deprecate the old one. Keep response shapes extensible (objects rather than bare arrays at the top level, so you can add metadata later). Decide early how unknown fields are treated on input, because that choice is difficult to reverse.
| Approach | How it looks | Strengths | Trade-offs |
|---|---|---|---|
| URL path version | /v2/orders | Obvious, easy to route and cache, easy to test in a browser | Whole-API version bumps; encourages large, infrequent breaking releases |
| Header or media-type version | Accept: application/vnd.example.v2+json | Clean URLs; can version per resource | Less visible, harder to debug and cache, more client friction |
| Query parameter version | /orders?version=2 | Simple to add | Easy to omit by accident; can fragment caching |
| Date-based version | A dated header chosen by the client | Fine-grained, predictable change history | Requires infrastructure to support many dated behaviours |
Do not keep every version alive forever
Each supported version multiplies testing, security patching and support work. Define up front how many versions are supported at once and for how long, and publish that policy. An API with no retirement path accumulates versions until the oldest ones become the biggest source of risk.
Deprecation that clients can actually act on
Deprecation fails when it is only an announcement in a changelog nobody reads. A workable process has several parts:
- Announce early with a specific retirement date and a migration guide that shows old and new side by side.
- Signal in the response itself, for example with a deprecation indicator and a sunset date in response headers, so developers notice during normal work.
- Measure usage per client and per endpoint. You cannot retire what you cannot see; usage data tells you who still needs a nudge.
- Contact affected clients directly, especially partners, before the date.
- Brown out if necessary: brief, scheduled failures shortly before retirement can surface forgotten dependencies safely.
- Retire on the stated date, or consciously extend it. Silent extensions teach everyone that dates are optional.
Contract testing and consumer awareness
Documentation says what you intended; consumers depend on what you shipped. Contract tests capture the exact request and response expectations of each consumer and run in your pipeline, so a change that would break someone fails the build instead of production. Even without a formal tool, keep a regression suite of recorded real requests. This fits naturally into the pipeline discipline in our CI/CD guide for small teams.
Internal vs. public APIs
Not every API needs heavy ceremony. An internal API consumed by a team that deploys in lockstep can change faster, provided consumers and producers can be updated together. Public and partner APIs, and any API consumed by mobile apps you cannot force-upgrade, deserve the strictest compatibility discipline, because you cannot control when clients update. Treat anything with a client you cannot redeploy as public.
Plan for the breaking change you will eventually need
Sometimes a model is simply wrong and compatibility must give way. When that happens, version at the smallest unit that makes sense, ship the new version side by side, migrate clients deliberately, and retire the old one on schedule. Doing this rarely and carefully is far better than avoiding it forever and letting a flawed design harden, which is how technical debt becomes permanent.
- A written definition of what counts as a breaking change
- Changes are additive by default; new fields and endpoints are optional for clients
- One consistent versioning approach is documented and applied
- Clients are told to ignore unknown fields and unknown enum values
- A stated policy for how many versions are supported and for how long
- Deprecation notices include dates, migration guides and in-response signals
- Per-client usage of deprecated endpoints is measured
- Contract or regression tests run in CI against real consumer expectations
Designing or evolving APIs that other teams depend on?
Talk to our engineering team about API design, versioning strategy and safe migration plans.
Frequently asked questions
What is a breaking change in an API?+
Any change that makes a correctly written existing client stop working without code changes: removing or renaming fields, changing types or meaning, making optional inputs required, tightening validation or changing documented defaults.
Is adding a new field a breaking change?+
Normally not, as long as clients ignore fields they do not recognise. State that expectation in your documentation. Adding enum values can be breaking if clients assume the set is closed.
Should I version by URL or by header?+
Both work. URL versioning is more visible and easier to route and debug; header versioning keeps URLs clean but is harder to see and cache. Choose one and apply it consistently.
How long should I support an old API version?+
Long enough for clients to migrate with notice, but with a published limit. The right length depends on how many clients you have and whether you can force them to update, as with mobile apps or partners.
How do I know who still uses a deprecated endpoint?+
Log and measure usage per client and per endpoint. That lets you contact remaining users directly before the retirement date.
Do internal APIs need versioning?+
Not always. If producer and consumers deploy together, you can change them in lockstep. Treat any API with consumers you cannot redeploy, such as mobile apps or partners, as public.
Written by
CodeSurge AI Engineering Team
The CodeSurge AI team designs and builds AI systems, SaaS products and enterprise integrations for clients in India, the UAE and beyond — this section shares the architecture patterns, cost drivers and implementation tradeoffs we work through on real projects.