Skip to content
Muhammet Şafak
tr
Web Development 3 min read

API versioning strategies and trade-offs

How do you evolve an API without breaking existing clients? A comparison of versioning strategies with their trade-offs, drawn from real-world experience.


Once you publish an API, you’ve essentially told the world “this contract will not change.” Change it, and the clients depending on you break. Don’t change it, and you’re stuck living with your design mistakes forever. API versioning is how you manage this tension — but there’s no single “right” way; you’re choosing between trade-offs.

Why is versioning necessary?

Say you need to change the response structure of a resource. Your user object currently looks like this:

{
  "id": 1,
  "name": "Muhammet Şafak",
  "created_at": "2019-01-15T10:00:00Z"
}

In the new version, name is going to be split into separate first_name and last_name fields. If you make this change in a single version, every client relying on the name field breaks immediately. Versioning lets the old and new structures coexist for a period of time.

Three common strategies

URI versioning

GET /api/v1/users
GET /api/v2/users

The most widely used approach. It’s trivially easy to tell which version a request targets just by looking at the URL — it shows up cleanly in logs, proxy rules, and documentation. The downside: it multiplies your URLs and raises the question “should I maintain separate v1/v2 paths for every resource?”

Header versioning

GET /api/users
Accept: application/vnd.myapi.v2+json

or a custom header:

X-API-Version: 2

The URL stays clean. But what happens if the client sends the header incorrectly or omits it entirely? Does it fall back to a default version? Return an error? That ambiguity becomes part of your contract.

Query parameter

GET /api/users?version=2

A practical starting point, but harder to maintain over time. Cache behavior becomes unpredictable; proxies and CDNs may not account for the query parameter.

What I prefer

On several projects I’ve settled on URI versioning: /api/v1/, /api/v2/. My reasoning is straightforward:

  1. Readability. The version you’re calling is visible right in the URL. Debugging a log line is instant — no guesswork.

  2. Documentation boundary. Each version can have its own separate documentation. You don’t have to hunt for the version to answer “how does this endpoint behave in v2?”

  3. Deployment flexibility. You can route v1 and v2 to separate controllers or route groups. No need to entangle the codebases.

Header-based versioning has theoretical elegance, but in practice, when a client misconfigures a header, tracking down the problem takes considerably longer.

Backward-compatible changes

Not every change requires a new version. Backward-compatible changes can be made within the current version:

  • Adding a new field to a response.
  • Adding an optional parameter.
  • Enriching error messages.

Breaking changes, on the other hand, require a new version:

  • Removing or renaming an existing field.
  • Adding a required parameter.
  • Fundamentally restructuring a response.
  • Changing a status code.

When do you retire old versions?

The answer depends on your client base. For an internal API, you can drop v1 quickly and mandate migration to v2. For a publicly exposed API, a deprecation window of at least 6–12 months is reasonable.

You can use HTTP headers to communicate the deprecation timeline:

Deprecation: true
Sunset: Sat, 31 Jan 2021 23:59:59 GMT

The Sunset header isn’t yet processed as a standard by browsers or most client libraries, but it still serves as a meaningful signal to client developers.

There’s no perfect versioning strategy. In my own projects I default to URL-based versioning because it’s the most visible approach for client developers and makes it trivial to look at a request and know exactly which version is in play. Header-based versioning may look more elegant on paper, but its invisibility during debugging has burned me more than once. Whichever approach you pick, the important thing is to apply it consistently and communicate clearly with your clients — a versioning strategy only works when it’s predictable.

Tags: #API
Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

Related Posts

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind