TL;DR
URL versioning (/v1/pets) is the most practical API versioning strategy for most teams. It’s visible, cacheable, and easy to test. Header versioning and content negotiation are more “pure” REST but add complexity. Modern PetstoreAPI uses URL versioning with semantic versioning and clear deprecation policies.
Introduction
When your API needs a breaking change—like changing the response format for /pets from a bare array to a wrapped object with pagination metadata—existing clients can break. How do you handle this?
API versioning is essential. The main strategies are URL versioning (/v1/pets vs /v2/pets), header versioning (Accept: application/vnd.petstore.v1+json), and content negotiation. Each has pros and cons.
For most teams, URL versioning is pragmatic, visible, and compatible with all HTTP tooling. Header versioning and content negotiation are conceptually cleaner but often unnecessarily complex.
Modern PetstoreAPI applies URL versioning, semantic versioning, and clear deprecation policies. Its current version is v1, with v2 planned for future breaking changes.
💡 If you’re building or testing REST APIs, Apidog helps you test multiple API versions, validate version-specific behavior, and ensure backward compatibility. You can maintain separate specs for each version and run tests against all versions simultaneously.
This guide covers the three main versioning strategies, their tradeoffs, and how to implement API versioning using Modern PetstoreAPI as a reference.
Why APIs Need Versioning
APIs evolve: you add features, fix bugs, and improve designs. Sometimes these changes break existing clients.
Breaking Changes
Examples of breaking changes:
1. Removing fields:
// v1
{"id": "123", "name": "Fluffy", "age": 3}
// v2 (breaking: removed age)
{"id": "123", "name": "Fluffy"}
2. Changing field types:
// v1
{"price": "19.99"}
// v2 (breaking: string to number)
{"price": 19.99}
3. Changing response structure:
// v1 (bare array)
[{"id": "123"}]
// v2 (breaking: wrapped object)
{"data": [{"id": "123"}], "pagination": {...}}
4. Changing URL structure:
// v1
GET /pet/123
// v2 (breaking: plural)
GET /pets/123
5. Changing authentication:
// v1: API key in query
GET /pets?api_key=xxx
// v2 (breaking: Bearer token)
GET /pets
Authorization: Bearer xxx
Non-Breaking Changes
Non-breaking changes include:
- Adding new endpoints
- Adding optional fields to requests
- Adding new fields to responses (clients should ignore unknown fields)
- Adding new query parameters
- Adding new HTTP methods to existing resources
The Versioning Decision
For breaking changes, you have two options:
- Force all clients to upgrade – Simple, but breaks integrations.
- Support multiple versions – More work, but maintains backward compatibility.
Most public APIs support multiple versions, letting clients migrate on their schedule.
URL Versioning
URL versioning puts the version number in the URL path.
How It Works
GET /v1/pets
GET /v2/pets
Each version is a different resource.
Pros
1. Visible and explicit:
The version is in the URL—easy to see in logs, browser history, and docs.
2. Easy to test:
curl https://petstoreapi.com/v1/pets
curl https://petstoreapi.com/v2/pets
3. Works with all HTTP tooling:
Browsers, proxies, and caches can route/cache based on URL.
4. Simple for clients:
Clients just update the URL, no header logic required.
5. Easy to deprecate:
You can remove /v1 endpoints without affecting /v2.
Cons
1. Not "pure" REST:
REST theory says /v1/pets/123 and /v2/pets/123 are the same resource, so URLs should not change.
2. URL pollution:
Multiple URL spaces: /v1/*, /v2/*, etc.
3. Harder to version individual resources:
You usually version the whole API, not just one endpoint.
Implementation
Major version in URL:
/v1/pets
/v2/pets
Avoid minor versions in URLs:
❌ /v1.2/pets (too granular)
✅ /v1/pets (major only)
Use semantic versioning internally:
- v1.0.0 – Initial release
- v1.1.0 – Non-breaking new fields
- v1.2.0 – Non-breaking new endpoints
- v2.0.0 – Breaking changes (new URL: /v2)
Modern PetstoreAPI uses /v1 as the current version.
Header Versioning
Header versioning puts the version in a custom HTTP header.
How It Works
GET /pets
API-Version: 1
GET /pets
API-Version: 2
The URL stays the same; the header specifies the version.
Pros
1. Clean URLs:
/pets is always the same.
2. More RESTful:
Resource identifiers don't change; only representations do.
3. Granular versioning:
Version individual resources:
GET /pets
API-Version: 2
GET /orders
API-Version: 1
Cons
1. Invisible:
Version isn’t in the URL, so logs and browser history don’t show it.
2. Harder to test:
curl -H "API-Version: 1" https://petstoreapi.com/pets
curl -H "API-Version: 2" https://petstoreapi.com/pets
3. Caching complexity:
Caches must consider the API-Version header (Vary: API-Version).
4. Client complexity:
Requires custom header logic.
5. Default version ambiguity:
You must define behavior when the header is missing.
Implementation
Custom header:
API-Version: 1
Or use Accept header:
Accept: application/vnd.petstore.v1+json
Include Vary header:
Vary: API-Version
This tells caches to treat versions separately.
Content Negotiation
Content negotiation uses the Accept header with custom media types.
How It Works
GET /pets
Accept: application/vnd.petstore.v1+json
GET /pets
Accept: application/vnd.petstore.v2+json
Pros
1. Most RESTful:
Uses HTTP standards for representations.
2. Multiple formats:
Supports both versioning and format:
Accept: application/vnd.petstore.v1+json
Accept: application/vnd.petstore.v1+xml
Cons
1. Complex:
Clients must understand media types and content negotiation.
2. Harder to test:
curl -H "Accept: application/vnd.petstore.v1+json" https://petstoreapi.com/pets
3. Poor tooling support:
Many clients/tools don’t handle custom media types well.
4. Caching complexity:
Caches must consider Accept header (Vary: Accept).
5. Overkill for most APIs.
Implementation
Vendor-specific media type:
Accept: application/vnd.petstore.v1+json
Response:
Content-Type: application/vnd.petstore.v1+json
Vary: Accept
How Modern PetstoreAPI Implements Versioning
Modern PetstoreAPI uses URL versioning with clear policies.
Current Version: v1
https://petstoreapi.com/v1/pets
https://petstoreapi.com/v1/orders
https://petstoreapi.com/v1/users
All endpoints are under /v1.
Version Response Header
All responses include the API version:
X-API-Version: 1.2.0
Shows the exact semantic version.
Deprecation Warnings
Deprecated versions add these headers:
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: <https://docs.petstoreapi.com/migration/v1-to-v2>; rel="deprecation"
-
Deprecation: Indicates deprecation -
Sunset: Removal date -
Link: Migration guide
Version Discovery
The root endpoint lists available versions:
GET https://petstoreapi.com/
{
"versions": [
{
"version": "v1",
"status": "current",
"docsUrl": "https://docs.petstoreapi.com/v1"
}
]
}
Semantic Versioning
Modern PetstoreAPI follows semantic versioning:
- Major (v1, v2): Breaking changes, new URL
- Minor (v1.1, v1.2): New features, backward compatible
- Patch (v1.1.1): Bug fixes, backward compatible
Only major versions appear in URLs.
Testing API Versions with Apidog
Apidog helps you test multiple API versions.
Import Multiple Versions
Import OpenAPI specs for each version:
petstore-v1.yaml → Environment: v1
petstore-v2.yaml → Environment: v2
Run Tests Against All Versions
Create test suites for both versions:
// Test v1
pm.environment.set("baseUrl", "https://petstoreapi.com/v1");
pm.sendRequest(pm.environment.get("baseUrl") + "/pets");
// Test v2
pm.environment.set("baseUrl", "https://petstoreapi.com/v2");
pm.sendRequest(pm.environment.get("baseUrl") + "/pets");
Validate Version-Specific Behavior
Test that v1 and v2 behave differently:
// v1 returns bare array
pm.test("v1 returns array", function() {
pm.expect(pm.response.json()).to.be.an('array');
});
// v2 returns wrapped object
pm.test("v2 returns wrapped object", function() {
pm.expect(pm.response.json()).to.have.property('data');
pm.expect(pm.response.json()).to.have.property('pagination');
});
Check Deprecation Headers
Verify deprecated versions include required headers:
pm.test("Deprecated version includes headers", function() {
pm.response.to.have.header("Deprecation");
pm.response.to.have.header("Sunset");
});
Version Deprecation Strategy
Deprecate old versions without breaking clients:
1. Announce Deprecation Early
Give clients at least 6–12 months notice:
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
2. Provide Migration Guide
Document all breaking changes and migration steps:
Link: <https://docs.petstoreapi.com/migration/v1-to-v2>; rel="deprecation"
3. Monitor Usage
Track which clients use deprecated versions:
X-API-Version: 1.2.0
X-Client-ID: abc123
Contact clients if needed.
4. Gradual Shutdown
Recommended timeline:
- Months 1–6: Announce deprecation
- Months 7–9: Add deprecation headers
- Months 10–11: Reduce rate limits for deprecated version
- Month 12: Remove deprecated version
5. Keep Documentation
Maintain docs for old versions even after removal for client reference.
Conclusion
URL versioning is the most practical API versioning strategy. It's visible, easy to test, and compatible with all HTTP tools. Header versioning and content negotiation are more “pure” REST but usually add unnecessary complexity.
Modern PetstoreAPI uses URL versioning with /v1 as the current version, semantic versioning, and clear deprecation policies—balancing pragmatism and good API design.
Use Apidog to test and validate multiple API versions, ensure backward compatibility, and facilitate smooth migrations.
FAQ
Should I use URL versioning or header versioning?
Use URL versioning unless you have a specific reason not to. It’s simpler, more visible, and easier to test. Header versioning is more “RESTful” but adds complexity most teams don’t need.
How many versions should I support simultaneously?
Support 2 versions maximum: current and previous. Supporting more increases maintenance. Give clients 6–12 months to migrate, then remove old versions.
Should I version from v0 or v1?
Start with v1. v0 implies instability. If your API isn’t stable enough for v1, don’t release it publicly yet.
Do I need to version every endpoint?
No. Only version when making breaking changes. Adding new endpoints doesn’t require a new version.
What about minor versions in URLs?
Don’t include minor versions in URLs. Use /v1, not /v1.2. Minor versions are backward compatible.
How do I handle version-specific bugs?
Fix bugs in all supported versions. Don’t force clients to upgrade for bug fixes.
Should I use semantic versioning?
Yes, internally. Track major.minor.patch, but expose only major in URLs. This supports non-breaking changes without disrupting clients.
What if I need to version just one endpoint?
With URL versioning, you typically version the whole API for consistency. Most teams accept this tradeoff for simplicity.
Top comments (0)