On this page
  1. Why version an API at all?
  2. Step 1 — classify the change before you ship it
  3. Step 2 — choose a scheme and keep it consistent
  4. Step 3 — run deprecations on a published schedule
  5. Step 4 — prove the contract with tests
  6. Where AI coding assistants get this wrong
  7. Checklist
  8. FAQ
    1. Should API versions go in the URL or a header?
    2. Is adding a field ever breaking?
    3. How long should a deprecation window be?
    4. Do internal APIs need versioning?
  9. Related topics
  10. Sources
tutorial

How to Version an API

A practical versioning process: choose a scheme, make additive changes without breaking clients, deprecate on a schedule, and prove the contract with tests.

Quick answer

  • A change is breaking if an existing client would need to change code: removing or renaming a field, changing a type, changing a status code, or making an optional parameter required.
  • Most changes are not breaking. Adding an optional field or a new endpoint is safe, which is why disciplined additive change delays the need for a new version.
  • The three common schemes are a path segment (/v2/...), a header, and a media type. Path segments are the easiest to see in logs and the easiest to use.
  • Version the contract in version control and generate or diff it, so a breaking change is caught in review rather than by a client.
  • Deprecate with a published window, a warning header, and monitoring of remaining usage; then remove it on a date you announced.

Why version an API at all?

Because clients you do not control need to keep working while you change. A version is a promise about which changes you will and will not make, and it is what makes it safe for someone else to build against your interface. Without one, every deployment is a potential outage for a client whose code assumed the old shape.

The goal is not to have many versions. It is to make almost every change without one, and to reserve a version bump for the changes that genuinely cannot be made compatibly. Teams that version constantly have usually not agreed on what a breaking change is; teams that never version are usually breaking clients without noticing.

Step 1 — classify the change before you ship it

Write the change down and ask whether an existing client would need to be modified. Removing a field, renaming one, narrowing a type, changing the meaning of an existing value, or requiring a parameter that was optional all break clients. Adding a new optional field, adding an endpoint, or accepting an additional value in an enum is safe in most cases — though the enum case requires clients to tolerate unknown values, which is worth stating in the contract.

Two categories are easy to miss. Changing validation is breaking if it rejects payloads that previously succeeded, because a working client starts failing. And changing error shapes breaks clients that branch on the error format, which is why error responses deserve the same contract discipline as success responses.

Step 2 — choose a scheme and keep it consistent

For public APIs, a path segment is the pragmatic default: it is visible in logs, trivial to route, and unambiguous to a client developer. Use a major version only — v1, v2 — because minor versions in the path invite drift and double the surface you maintain. Header-based and media-type versioning are technically more pure and harder to operate, and content negotiation failures are a common support burden.

Whatever you choose, keep it uniform and document the mapping from version to contract. Version the schema itself — an OpenAPI document per version, checked in — so the shape of v1 is recoverable after the code has moved on. That document is what lets you tell a client exactly what changed, and it is the artifact a contract test runs against.

Decision order for an API change:
1. Can the change be additive?  -> ship it in the current version.
2. Can the old behaviour be kept as a deprecated alias for a window?
     -> ship the new form, keep the old, announce removal.
3. Does the change force existing clients to update now?
     -> new major version, new path prefix, both served in parallel.

Announce with:
  Deprecation: true
  Sunset: Wed, 01 Apr 2026 00:00:00 GMT
  Link: <https://docs.example.com/api/v2>; rel="successor-version"

Step 3 — run deprecations on a published schedule

A deprecation is a promise with a date. Start it by returning a warning header and documenting the replacement, keep both behaviours working for a stated period, then remove the old one on the announced date. Two things make this work in practice: the window is long enough that a client with a release cycle can plan, and you actually measure who is still calling the old shape so you can warn them directly rather than hoping they read the changelog.

Instrument the old version’s traffic by client where you can, and treat a non-zero count at the deadline as a decision point rather than something to ignore. Extending the window deliberately is fine; quietly leaving code in place forever is how a codebase accumulates three versions of everything and the cost lands on your own team.

Step 4 — prove the contract with tests

Add a test that compares the served response against the stored schema for each version, so a field disappearing is caught by CI rather than by a user. This is the single highest-value control for API stability, because it turns an invisible breaking change into a failing build. It also makes adding optional fields safe to do confidently.

Then keep a small suite of consumer-driven checks for the clients you care most about: a handful of real request/response pairs that must keep working. Where clients generate their code from the schema, regenerating and diffing is a strong check as well. The point in all cases is that the contract is asserted, not remembered.

Where this bites vibecoders

Generated endpoints change shape freely as the prompt changes: a field is renamed, a response is restructured, a status code shifts. Nothing in the generation process knows a client exists. If you have external consumers, the schema diff in CI is the only reliable guard, and it is the thing no assistant will add unprompted. Ask for it explicitly, and commit the schema per version so there is something to compare against.

Where AI coding assistants get this wrong

  • Renaming a response field because the new name reads better, breaking every client.
  • Making a previously optional parameter required in a patch release.
  • Removing an endpoint without a deprecation window or a warning.
  • Tightening input validation and rejecting payloads that used to work.
  • Versioning per endpoint rather than per contract, producing inconsistent surfaces.
  • Having no schema diff or contract test, so breaking changes ship silently.

Checklist

  • Classify each change as additive or breaking before shipping it.
  • Keep a stored schema per version and diff it in CI.
  • Serve a new major version in parallel rather than replacing in place.
  • Send deprecation and sunset headers on anything on the way out.
  • Measure who still calls the old shape and tell them.
  • Remove the old version on the announced date.

FAQ

Should API versions go in the URL or a header?

A path segment is the practical default for public APIs: visible in logs, easy to route, no content-negotiation edge cases. Headers and media types are technically cleaner and harder to operate, and clients frequently get the negotiation wrong. Choose one, apply it uniformly, and document it.

Is adding a field ever breaking?

It can be, if a client rejects unknown fields or uses exhaustive matching on an object shape. Well-written clients ignore unknown fields, and stating that expectation in the contract makes additions safe. It is still worth announcing, because a strict client in someone else’s language will find it.

How long should a deprecation window be?

Long enough that a client with a normal release cycle can plan: a quarter is a common choice for a public API, longer where clients are enterprises. The window matters less than the announcement, the warning header, and direct notice to the remaining callers. A silent deadline is not a deprecation.

Do internal APIs need versioning?

Internal contracts break less often and are cheaper to fix, so a lighter discipline is reasonable — additive changes and a schema diff, without formal versions. The exception is an internal API consumed by a client you cannot redeploy at will, such as a mobile app already installed on devices, which needs the same treatment as a public one.

Sources

Share: