API Versioning Without the Regret
The ladder from additive changes to real versions: tolerance rules that make 90% of changes free, deprecated-field telemetry, Sunset headers, brown-outs, and the internal-API discount.
Nobody versions an API on day one, and nobody regrets anything harder by year three. The first breaking change arrives innocently — a field rename, a status enum that grew — and suddenly you discover that "the API" is actually a promise you made to every client that ever shipped, including the mobile app version from eighteen months ago that 4% of your users refuse to update. This post is the versioning strategy I converged on after paying that bill a few times.
The first rule: version as late as possible, evolve as long as possible
A new API version is the most expensive tool in the box — every version you support multiplies test surface, documentation, and mental load, forever. So the strategy is a ladder, and new-version is the top rung, not the first:
Rung 1: additive changes (free, use constantly)
Adding a field to a response, a new optional request parameter, a new endpoint, a new enum value where you've documented that clients must tolerate unknowns — none of these break a well-behaved client, so none need a version. This only works if you've set the contract expectations early, so write them into your API docs on day one:
- Clients MUST ignore response fields they don't recognize.
- Clients MUST treat unknown enum values as a documented fallback (e.g. unknown order status → "pending review").
- Nothing about field order is promised, ever.
These three sentences are the highest-leverage documentation you'll ever write. They convert 90% of future changes from "breaking" to "Tuesday". (They're also why gRPC/protobuf feels so evolvable — the tooling enforces this posture; in REST you have to declare it.)
Rung 2: tolerant evolution tricks
Need to rename customer_name to customer.full_name? Ship both for a season. Need to change semantics? Add the new field alongside (total → total_minor), mark the old one deprecated in the schema, and watch usage metrics until the old one goes quiet. Boring, unglamorous, and it avoids a version bump entirely. The prerequisite: per-field or at least per-endpoint usage telemetry — you can't retire what you can't measure. A middleware that logs deprecated-field access per API key costs an afternoon and pays for itself the first time you can email exactly the seven affected integrators instead of everyone.
Rung 3: the actual new version
Reserved for changes you can't do additively: authentication overhauls, resource remodeling, pagination scheme changes. When it happens, decide these things explicitly:
Where the version lives:
URL path /v2/orders ← my default: visible, cacheable, curl-able
Header Api-Version: 2 ← cleaner URLs; invisible in logs & browser
Date-based Version: 2026-03-01 ← Stripe-style; superb but demands serious
tooling (request/response transformers per date)
How many live at once: two. v_current and v_previous. Three is a support org.
How long v1 lives: a stated window (12–18 months B2B), in the docs, in the
deprecation emails, and in Sunset headers.
On implementation: resist the copy-paste-the-controllers instinct. The maintainable shape is one internal core + thin version adapters — request transformers mapping v1 shapes into today's internal model, response transformers mapping back out. Stripe's date-versioning is this pattern industrialized. In Laravel terms: v1 and v2 route files, shared actions/services, per-version API Resources doing the shaping.
Killing a version without killing trust
Deprecation is a communications project wearing an engineering costume. The sequence that works:
- Announce with a date, in changelog + email to affected keys (you have the telemetry from rung 2, so "affected" is a real list, not "everyone").
- Advertise in-band:
Deprecation: trueandSunset: Sat, 01 Mar 2026 00:00:00 GMTheaders on every v1 response, plus aLinkto the migration guide. Machines notice headers; humans notice emails; you need both. - Brown-outs before blackout. A few scheduled one-hour windows where v1 returns 410 — weeks before the real sunset — flush out the integrations nobody remembered. Cruel? The 3 a.m. hard cutoff without rehearsal is crueler.
- 410 Gone with a helpful body at sunset, kept forever. A dead version that 404s teaches nothing; a 410 with a migration link converts stragglers.
The internal-API discount
Everything above is for APIs with clients you don't control. Between your own services, or backend-for-frontend endpoints where you ship both sides, skip the ceremony: keep additive discipline (rung 1 is free insurance), deploy consumers before producers when removing things, and use contract tests instead of version numbers. Versioning internal APIs like public ones is how teams end up maintaining v1-through-v4 of an endpoint with exactly one caller.
The compressed wisdom: declare tolerance early, evolve additively for years, version rarely, deprecate loudly, and measure everything in between. APIs age like buildings — the ones that last were designed for renovation, not demolition.
About to make your first breaking change — or maintaining four versions and losing the will? A versioning strategy session is cheaper than either mistake.