Skip to content
routebase
API DesignVersioningBreaking Changes

Versioning APIs Without Breaking Your Consumers

· The Routebase Team

A short stair bridging an older sand-colored platform to a newer one stacked above it, the joining step lit, the old platform dissolving into loose particles at its far edge

Nobody versions an API for fun. Versioning is what you do when you need to change a contract that other people's code depends on — and every strategy is just a different way of distributing the pain between you and your consumers. The goal isn't a clever version scheme. It's breaking people as rarely, as visibly, and as considerately as possible.

Know what actually breaks

Half of all versioning anxiety comes from not having a shared definition of "breaking." Additive changes are safe if your consumers follow the must-ignore rule (tolerate unknown fields — worth stating explicitly in your docs):

  • Adding a new endpoint
  • Adding an optional request field or parameter
  • Adding a response field
  • Adding a new enum value where consumers were told to expect additions

Breaking changes are everything that invalidates a working consumer:

  • Removing or renaming a field, endpoint, or parameter
  • Making an optional field required
  • Changing a field's type or format ("42"42)
  • Narrowing validation (max length 500 → 100)
  • Changing error shapes or status codes consumers branch on
  • Changing defaults or semantics — same request, different behavior

The subtle ones do the damage. Nobody renames a field by accident, but tightening validation "because no legitimate value is that long" breaks the consumer who had one. If a change would make any currently-valid request or any currently-parsed response invalid, it's breaking — no matter how reasonable it feels.

Each change starts on the side intuition files it and ends on the side the test puts it — and three of the six cross over. Narrowing a validation rule and changing a default feel like housekeeping and break working consumers; a new enum value feels risky and is safe as long as consumers were told to expect additions. Only one question decides it: does anything that worked yesterday stop working?

Evolve additively first

The cheapest version bump is the one you avoid. Most redesigns can ship as additive evolution: add the new field alongside the old, dual-write both, migrate consumers, and only then deprecate the original.

# Instead of renaming owner_email (breaking):
properties:
  owner_email:
    type: string
    deprecated: true
    description: "Deprecated: use `owner.email` instead. Removed after 2026-12-01."
  owner:
    type: object
    properties:
      email: { type: string }
      name:  { type: string }

This is more work for you — two fields, a sync rule, a removal ticket. That's the correct direction for the trade-off: one team doing extra work so that N consumer teams don't have to do a forced migration. An API version is a promise, and additive evolution is how you keep it while still moving.

When you must break: make the boundary explicit

Some changes can't be additive — a security fix in the auth model, a resource model that's fundamentally wrong. Then you need a version boundary, and the mechanics matter less than people argue about online:

  • URL versioning (/v2/projects) — visible in every log line, trivially routable, impossible to send by accident. The pragmatic default.
  • Header versioning (Api-Version: 2026-07-01) — keeps URLs stable and supports date-based pinning, at the cost of being invisible in casual use.

Two rules matter more than the mechanism. Version the whole API surface, not individual endpoints — a consumer on "v2" should never have to remember which five endpoints have a v2. And v1 keeps working the day v2 ships — a version boundary is a migration path, not a cutover.

Run deprecations like you mean them

A deprecation is a project with a timeline, not a doc footnote. The sequence that keeps consumers on your side:

  1. Announce with a date — changelog, spec annotation (deprecated: true), and machine-readable response headers:

    HTTP/1.1 200 OK
    Deprecation: true
    Sunset: Tue, 01 Dec 2026 00:00:00 GMT
    Link: <https://api.example.com/docs/migrations/v2>; rel="deprecation"
  2. Give a real window — commonly 6–12 months for public APIs; what matters is that it reflects your consumers' release cadence and that you honor it.

  3. Watch actual usage — traffic on deprecated surfaces tells you who hasn't migrated. Silence in your metrics is the only acceptable green light for removal.

  4. Remove loudly — a clear 410 Gone with a pointer to the migration guide beats a mysterious 404.

A deprecation run in one picture: the Deprecation header starts the migration, traffic on the old surface drains — and it is still above zero when the announced Sunset date passes. The date starts the clock; the flat zero afterwards is what actually clears the removal, and the removal itself is a loud 410 Gone, not a mysterious 404.

Catch breaks before consumers do

The uncomfortable truth about breaking changes: most of them ship unintentionally. A refactor renames a DTO property, a framework upgrade changes serialization, a well-meaning cleanup deletes a "clearly unused" field — and no human notices, because humans don't diff contracts.

Machines should. If your API has a spec as its source of truth, every change can be diffed against the previous version and classified — additive or breaking — before it merges. Contract testing closes the loop from the other side, verifying the implementation still honors what the spec promises.

Versioning policy sets the rules. Enforcement is what makes consumers trust them — and trust, not the number in your URL, is what API versioning is actually about. For the broader design context, start with the API design principles guide.

Ready to ship on it?

Routebase is live. Design your API once — docs, mocks, tests, and monitoring all follow from the same source.

14-day Pro trial — no credit card required.