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