API versioning discussions focus on the wrong question. Whether the version goes in the URL, a header or a query parameter matters far less than the organisational question underneath: how many versions will you actually maintain, and what happens to customers who never migrate. Answer that first and the technical choice mostly follows.
01Most breaking changes are optional
Before choosing a versioning scheme, reduce how often you need it. A large share of changes teams treat as breaking are additive if approached differently. Adding a field is not breaking if clients ignore unknown fields. Adding an optional parameter is not breaking. Adding a new enum value is not breaking if clients were told to expect unknown values.
The discipline is establishing those expectations in your contract from day one - documenting that clients must tolerate unknown fields and unrecognised enum values - and then designing changes to be additive. Teams that do this ship for years without a major version bump.
- Never remove or rename a field in place - add the new one alongside.
- Never change a field's type or the meaning of an existing value.
- Never make an optional request parameter required.
- Never tighten validation on inputs you previously accepted.
- Document from the start that clients must ignore unknown fields.
02The schemes, honestly compared
When a breaking change is genuinely unavoidable, these are the realistic options. The differences in developer experience are smaller than advocates claim; the differences in operational burden are larger.
| Approach | Strengths | Weaknesses |
|---|---|---|
| URL path (/v1/, /v2/) | Obvious, cacheable, trivial to route and debug | Version applies to the whole API; encourages big-bang jumps |
| Custom header | Clean URLs, per-request granularity | Invisible in logs and browsers; easy to omit accidentally |
| Date-based | Fine-grained, customer pins a date, gradual evolution | Requires transformation layer; complex internally |
| No versioning | Zero overhead; forces additive discipline | Only viable if you genuinely never break |
03Why date-based versioning scales best
The approach used by the payment and communications APIs that manage this well is date-based pinning: a customer's account is pinned to the API behaviour as of a specific date, and each breaking change becomes a dated revision. New integrations get current behaviour; existing ones keep working indefinitely.
The cost is real and concentrated internally. You maintain a chain of transformations from current internal representation back to each supported date, and every request passes through the relevant ones. That is meaningful engineering investment, and it only pays off when you have many customers with genuinely different upgrade timelines. For a small API with a handful of consumers, URL versioning is the sane choice.
04Deprecation that works
Announcing a sunset date is not a deprecation process. Customers who are not actively building against your API will not read the announcement, and the ones who do will defer the work until it is urgent. A process that works assumes nobody is paying attention.
Instrument usage per version per customer so you know exactly who is affected rather than guessing. Contact them directly through the account relationship, not just a changelog. Send Deprecation and Sunset headers so the signal appears in their own logs. Then run scheduled brownouts - brief, announced periods where the old version returns errors - because nothing surfaces a forgotten integration like a controlled failure months before the real cutoff.
- Track usage per version per customer; never guess at impact.
- Reach out through account channels, not only public changelogs.
- Emit Deprecation and Sunset response headers.
- Run announced brownouts well before the deadline.
- Expect stragglers and decide your policy for them in advance.
05Version the contract, not the implementation
A frequent structural mistake is forking the codebase per version - a v1 controller and a v2 controller with duplicated business logic that drifts apart until a bug fixed in one silently persists in the other.
Keep one internal implementation representing current truth, and treat versions as a translation layer at the boundary. Requests are transformed forward into the current shape, responses transformed back into the version the caller expects. Business logic exists once. This is more work to set up and dramatically less work to maintain, and it is what makes supporting several versions sustainable at all.
Topics
Lena Voss
Lead Architect · SyncTrix
Writes about the engineering decisions behind production systems - architecture, delivery and the trade-offs that only show up at scale.
Building something like this?
SyncTrix engineers AI, SaaS, platform and cloud systems for enterprises and high-growth teams. Tell us what you're shipping and we'll scope it with you.