API · Rest

REST API Versioning — A Field Guide

When a REST API actually needs a new version versus when a backward-compatible field addition will do, and the deprecation discipline that makes either approach survivable.

John Kihiu12 min read

The most expensive mistake in API versioning isn't picking the wrong versioning scheme — it's treating every change as if it needs a new version, or treating no change as if it ever will. A version bump is a promise to maintain two (or more) parallel contracts indefinitely, or to force every consumer to migrate on your timeline; it's worth reserving for changes that are genuinely breaking, and building the habit of making changes backward-compatible whenever that's actually possible.

What actually counts as a breaking change

Adding a new optional field to a response is not breaking — well-written clients ignore fields they don't recognize. Removing a field, renaming a field, changing a field's type (a string that used to be a number), or changing the meaning of an existing status code are all breaking, because any of them can silently corrupt a client that was written against the old contract without necessarily throwing an obvious error. The dividing line is whether an existing, unmodified client continues to behave correctly against the new response — if yes, ship it without a version bump; if no, it needs one.

Additive changes need documentation discipline more than a version bump

A new field doesn't require a new version, but it does require clients to be written defensively — deserializing unknown fields without erroring, rather than using strict schema validation that rejects any field not explicitly declared. Document this expectation for your consumers; it's what lets you add fields freely without every addition becoming a coordination exercise.

The decision that actually forces a version: changing a promise you already made

A version bump is warranted when you need to change something a client is already correctly relying on — not add to it, change it. Removing a deprecated field after its sunset date, changing pagination from offset to cursor-based, restructuring nested resources, or changing authentication requirements are the recurring examples. The test I use: if I can describe the change as "and old clients will now behave incorrectly, not just miss out on something new," it needs a version; if the honest description is "old clients get the same behavior they always had, plus optionally more," it doesn't.

Running two versions in parallel is the real cost, not choosing a URL scheme

Whatever mechanism you use to expose a version (this is covered in depth in a companion piece on versioning strategies), the actual expense of versioning is operational: two code paths to test, two sets of behavior to reason about when debugging, and a deprecation clock that someone has to actually track and enforce. Teams that version cheaply on paper and then never deprecate anything end up maintaining every version they've ever shipped, forever — which is a slower, quieter failure than an API with no versioning at all, because nobody notices the cost accumulating until the fifth version is still receiving traffic three years later.

HTTP · DEPRECATION HEADER
HTTP/1.1 200 OK
Deprecation: true
Sunset: Wed, 01 Jan 2027 00:00:00 GMT
Link: <https://api.example.com/docs/migrating-v1-to-v2>; rel="deprecation"

Deprecation needs a forcing function, not just a header

The Deprecation and Sunset headers (both now standardized in RFC 9745 and RFC 8594 respectively) tell a client an endpoint is going away and when — but a header alone doesn't make anyone migrate. Track per-consumer usage of deprecated endpoints, reach out directly to the accounts still calling them as the sunset date approaches, and actually turn the endpoint off on the announced date. An API that has announced ten deprecations and enforced none has taught its consumers, correctly, that deprecation notices from you are not real deadlines.

Don't version the whole API for a change that affects one endpoint

A single breaking change to one resource doesn't require bumping every other unrelated endpoint to a new version number — that forces consumers who only use the unaffected endpoints into an unnecessary migration. Per-resource or per-endpoint versioning (or feature flags for the specific breaking change) scopes the blast radius to what actually changed.

Wrapping up

Version only for genuinely breaking changes — changing behavior an existing client correctly relies on — and default to additive, backward-compatible changes for everything else. The real cost of versioning isn't the mechanism you choose to signal it; it's the deprecation discipline required to eventually retire the old version instead of running every version you've ever shipped forever.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.