How to Version an API: Strategies, Best Practices, and Common Mistakes

Versioning lets you evolve an API without breaking the clients that already depend on it. Get it right and you can ship improvements for years; get it wrong and one field rename takes down every integration overnight. This guide covers the main strategies, how to decide when a new version is warranted, and the mistakes that bite teams most often.

Why API versioning matters

Once a client integrates with your API, your response shapes and request contracts become a promise. Other people's code parses your JSON, depends on your status codes, and assumes your endpoints keep behaving the same way. A version is how you signal that the contract changed, so consumers can opt in deliberately instead of discovering a breakage in production. If you are still pinning down what an API even guarantees, refresh the concept in what is an API. Versioning exists because backward compatibility is hard, and pretending a contract never changes is how integrations rot.

What counts as a breaking change

You only need a new version when you make a breaking change: something that can cause a previously working client to fail. Knowing the line between breaking and non-breaking is the single most important skill in versioning.

Changes that are generally breaking:

  • Removing or renaming a field, endpoint, or query parameter.
  • Changing a field's type (for example, a string to a number) or its meaning.
  • Making a previously optional request field required, or tightening validation rules.
  • Changing default values, error formats, or HTTP status codes clients rely on.

Changes that are usually non-breaking (additive):

  • Adding a new optional field to a response.
  • Adding a new endpoint or a new optional request parameter.
  • Adding a new value to an enum, but only if clients are documented to ignore unknown values.

Design clients to tolerate additive change from day one: if consumers ignore unknown JSON fields, most of your evolution never needs a version bump.

The three main versioning strategies

URI path versioning

The version lives in the URL, such as /v1/users or /v2/users. This is the most common and most discoverable approach: visible in logs, easy to test in a browser, trivial to route at a gateway, and unambiguous when someone pastes a URL into a ticket.

The tradeoff is that the URL no longer strictly identifies a single resource: /v1/users/42 and /v2/users/42 point at the same user through two contracts. Purists dislike this, but the operational clarity is why large public APIs lean on it anyway.

Header / media-type versioning

The version travels in a request header instead of the path. Two patterns exist: a custom header such as Api-Version: 2, or content negotiation via the Accept header with a vendor media type:

Accept: application/vnd.example.v2+json

This keeps URLs stable and is the more REST-faithful approach. The cost is discoverability: you cannot exercise it from a browser address bar, and caching layers must vary on the header. A missing or malformed header that silently falls back to a default version is a common source of confusion.

Query parameter versioning

The version is a query string, such as /users?version=2. It is simple to add and easy to default, but it muddies caching and analytics and mixes a contract selector in with ordinary filtering parameters. Fine for internal tools, but rarely the best choice for a public API.

Which one should you pick?

For a public, widely consumed API, URI path versioning is the pragmatic default for its visibility and ease of debugging. Reach for header versioning when URL stability and strict REST semantics matter more, and your clients reliably set the header. Avoid query versioning unless the API is internal and short-lived. Whatever you choose, apply it everywhere; mixing schemes across endpoints is its own failure mode.

Choosing a version number scheme

Most HTTP APIs expose only a major version (v1, v2) on the wire, because consumers should never have to react to a non-breaking change. Reserve a new public version for breaking changes only.

Behind that, many teams track the full release with semantic versioning, or MAJOR.MINOR.PATCH, where MAJOR signals breaking changes, MINOR adds backward-compatible features, and PATCH covers backward-compatible fixes. See semantic versioning explained for the full convention, and compare version strings or check ordering with the SemVer Comparator. The common pattern: clients pick the major version on the wire, while changelogs and SDK packages use full SemVer internally.

Deprecating and retiring an old version

Adding a version is easy; removing one is where teams lose trust. Retire on a published schedule, never silently.

  1. Announce the deprecation in your changelog and docs, with a concrete sunset date, often months out for a public API.
  2. Signal it on the wire. Use the standard Deprecation header and the Sunset header (an HTTP-date marking when the version stops working) so clients can detect it automatically.
  3. Measure usage before you pull it; if a major integration is still on the old version, reach out directly.
  4. Return a clear error after sunset. Respond with 410 Gone for a removed version rather than a vague 404 or 500. The differences between codes are summarized in the HTTP Status Codes reference.

Run as few concurrent versions as you can; every live one is code you keep patching for security and bugs.

Document every version

A version without docs is a trap. Maintain a complete specification per supported version plus a changelog stating exactly what changed and why. An OpenAPI description keeps this machine-readable and can generate client SDKs; see the OpenAPI documentation guide. Pair the spec with migration notes that map old fields to their replacements, so consumers can upgrade without reverse-engineering your diffs.

Common mistakes to avoid

  • Versioning everything. Bumping the major version for additive changes forces needless migrations. Reserve new versions for breaking changes only.
  • No default and no validation. If a header scheme silently defaults to the latest version, clients break the moment you ship a new one. Require an explicit version or pin the default forever.
  • Mixing strategies. Path on some endpoints, header on others, is confusing and hard to route. Standardize.
  • Maintaining too many versions. Each multiplies your test surface, security patching, and support load.
  • Silent removal. Deleting a version with no deprecation window or Sunset signal destroys consumer trust fast.
  • Treating refactors as breaking. Internal changes that do not alter the contract should never bump the public version.

The throughline: version only when the contract genuinely changes, make every change observable, and give consumers a clear, scheduled path forward. Do that and your API can evolve for years without breaking its users.

Frequently Asked Questions

Only when you introduce a breaking change, meaning something that can cause an existing client to fail. That includes removing or renaming fields, changing a field's type or meaning, making an optional field required, or altering error formats and status codes that clients depend on. Additive changes like new optional fields or new endpoints are backward-compatible and should not trigger a version bump.

For a public, widely consumed API, URI path versioning (such as /v1/users) is the pragmatic default because it is visible in logs, testable in a browser, and easy to route. Header or media-type versioning keeps URLs stable and is more REST-faithful, but it is less discoverable and easier to forget. Pick one scheme and apply it consistently across every endpoint.

Usually not. Most HTTP APIs expose only the major version on the wire because consumers should never have to react to non-breaking changes. Full semantic versioning is best kept internal, for your changelog and SDK packages, while clients select just the major version in the URL or header.

Announce the deprecation in your docs and changelog with a concrete sunset date and a generous window. Signal it on the wire using the Deprecation and Sunset headers so automated clients can detect it, measure remaining usage before removing it, and return a clear 410 Gone after sunset rather than a vague 404 or 500.

As few as possible. Every live version is code you must keep patching for security and bugs, plus extra documentation, test surface, and support burden. Aim to support the current version and at most one prior version, with a published timeline that moves consumers forward.