APIs evolve. New features are added, existing endpoints change, and eventually, breaking changes are unavoidable. API versioning is the practice of managing these changes without breaking existing clients. This article covers the major versioning strategies and when to use each.
The Core Challenge
Once an API is public, changing it is risky. A client might depend on the current response format, and changing it could break the client's application. Versioning gives clients a choice: stay on the old version or upgrade to the new one.
Ideally, changes are backward compatible -- adding fields, relaxing validation, or extending functionality. But sometimes breaking changes are necessary: removing a field, changing a data type, or restructuring an endpoint.
URL Path Versioning
The most common and straightforward approach:
/api/v1/users
/api/v2/users
**Pros:**
**Cons:**
**Best for:** Public APIs where simplicity and discoverability are priorities. Used by Twitter, Stripe, and GitHub.
Query Parameter Versioning
Include the version as a query parameter:
/api/users?version=1
/api/users?version=2
**Pros:**
**Cons:**
**Best for:** Internal APIs or APIs where URL cleanliness is a priority over visibility.
Header Versioning
Include the version in an HTTP header:
GET /api/users
Accept: application/vnd.myapi.v1+json
Or a custom header:
GET /api/users
X-API-Version: 1
**Pros:**
**Cons:**
**Best for:** REST purists who want to follow HTTP semantics strictly. Used by GitHub (in the past) and Twilio.
Choosing a Strategy
| Strategy | Visibility | Complexity | RESTful | Caching | Routing |
|----------|------------|------------|---------|---------|---------|
| URL Path | High | Low | Moderate | Simple | Simple |
| Query Param | Medium | Low | Moderate | Complex | Complex |
| Header | Low | Medium | High | Simple | Complex |
There is no universally correct choice. Consider your audience:
Semantic Versioning for APIs
Apply semantic versioning principles to your API:
In practice, most APIs only expose the major version and use minor/patch versions internally for documentation purposes.
Supporting Multiple Versions
When you support multiple API versions, you need a strategy for maintaining old versions:
**Deprecation policy.** Announce deprecation well in advance. Give clients at least 6-12 months to migrate. Include deprecation warnings in response headers:
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Deprecation: true
**Minimum version support.** Support a minimum number of active versions (typically 2-3). Drop the oldest when a new one is introduced.
**Version sunset.** Actually retire old versions. Maintaining versions indefinitely is expensive. Be clear about your timeline and enforce it.
Implementation Patterns
**Code branching.** Maintain separate code for each version. Simple but leads to code duplication. Use only for small, stable APIs.
**Conditional logic.** Single codebase with version checks. More maintainable but can become complex:
if version == 1:
return serialize_v1(data)
elif version == 2:
return serialize_v2(data)
**Interceptor/middleware.** Transform responses between versions at the middleware layer. Keeps core logic version-agnostic:
if request_version == 1:
response = transform_to_v1(handler.handle(request))
elif request_version == 2:
response = transform_to_v2(handler.handle(request))
**Adapter layer.** Use the adapter pattern to translate between internal models and versioned API responses. The cleanest approach for complex APIs.
Avoiding Versioning Altogether
The best versioning strategy is not needing one. Design your API to be extensible from the start:
GraphQL eliminates many versioning concerns by letting clients specify exactly what they need.
Summary
API versioning is a practical necessity for evolving APIs. URL path versioning is the most common and practical approach. Support a limited number of versions and communicate deprecation timelines clearly. Design your API to minimize breaking changes through extensibility. When possible, prefer backward-compatible additions over new versions.