API Versioning

Strategies for versioning REST and GraphQL APIs — URL versioning, headers, semantic versioning, backward compatibility, and deprecation.

Why version APIs?

When multiple consumers depend on an API, any change to its contract can break existing integrations. Versioning allows the API to evolve in a controlled way, giving consumers time to adapt.

Without a clear versioning strategy, teams end up in one of two situations: either they can’t change anything for fear of breaking consumers, or they break things constantly without realizing it.

Versioning strategies

URL versioning

The most common and visible strategy. The version is included directly in the path:

GET /api/v1/products
GET /api/v2/products

Advantages:

  • Extremely explicit — there’s no ambiguity about which version is being used
  • Easy to route in API Gateways and load balancers
  • Simple to document and communicate

Disadvantages:

  • Can lead to code duplication if not structured well internally
  • Consumers must update URLs when migrating

Header versioning

The version is specified in a custom HTTP header or in the Accept header:

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

Or with a custom header:

GET /api/products
X-API-Version: 2

Advantages:

  • URLs remain clean and stable
  • Allows for more sophisticated content negotiation

Disadvantages:

  • Less visible — requires inspecting headers to know which version is being used
  • Harder to test from a browser

Semantic versioning (SemVer)

Applies Semantic Versioning principles to the API:

  • MAJOR (v1 → v2): Breaking changes — incompatible with previous versions
  • MINOR (v1.1 → v1.2): New backward-compatible features
  • PATCH (v1.1.0 → v1.1.1): Bug fixes with no functional changes

In practice, public APIs usually expose only the MAJOR version in the URL (/v1/, /v2/) and handle MINOR/PATCH internally.

Backward compatibility

A change is backward compatible if existing consumers can keep working without modifications. Examples of compatible changes:

  • Adding a new optional field to a response
  • Adding a new endpoint
  • Adding a new optional query parameter
  • Adding a new value to an enum (carefully)

Examples of breaking changes:

  • Removing a field from the response
  • Renaming an existing field
  • Changing a field’s type (string → number)
  • Making a previously optional field required
  • Changing the semantics of an existing field

Deprecation strategies

When you need to retire an old version:

1. Early communication

Announce the deprecation with enough lead time (at least 3-6 months for public APIs):

Deprecation: true
Sunset: Sat, 01 Mar 2025 00:00:00 GMT
Link: <https://docs.api.com/migration/v2>; rel="successor-version"

2. Coexistence period

Keep both versions running in parallel during the transition period. Monitor traffic on the old version to know when it’s safe to retire it.

3. Responses with warnings

Include warning headers in responses from the deprecated version:

Warning: 299 - "API v1 is deprecated. Please migrate to v2 by 2025-03-01"

4. Final sunset

Once the sunset date has passed, calls to the old version can return 410 Gone with a body explaining how to migrate.

Best practices

  • Version from day one — it’s much easier to start with /v1/ than to add versioning later
  • Document changes — keep a clear changelog per version
  • Use feature flags internally — to handle differences between versions without duplicating code
  • Monitor usage by version — know how many consumers use each version before deprecating
  • Define a support policy — how many versions you maintain simultaneously and for how long