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
Your API needs a breaking change. You’re changing the response format for /pets from a bare array to a wrapped object with pagination metadata. Existing clients will break. What do you do?
You need API versioning. But which strategy? URL versioning (/v1/pets vs /v2/pets)? Header versioning (Accept: application/vnd.petstore.v1+json)? Content negotiation? Each approach has passionate advocates and strong opinions.
The answer: URL versioning wins for most teams. It’s pragmatic, visible, and works with all HTTP tooling. Header versioning and content negotiation are theoretically cleaner but add complexity that most teams don’t need.
Modern PetstoreAPI uses URL versioning with semantic versioning and clear deprecation policies. The current version is v1, with v2 planned for future breaking changes.
In this guide, you’ll learn the three main versioning strategies, their tradeoffs, and how to implement versioning correctly 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
Changes that break existing clients:
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
Changes that don’t break clients:
- 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
When you need a breaking change, you have two options:
1. Force all clients to upgrade - Simple but breaks existing integrations
2. Support multiple versions - More work but maintains backward compatibility
Most public APIs choose option 2. Versioning lets you evolve the API while giving clients time to migrate.
URL Versioning
URL versioning puts the version number in the URL path.
How It Works
GET /v1/pets
GET /v2/pets
The version is part of the resource identifier. Different versions are different resources.
Pros
1. Visible and explicit
The version is in the URL. You can see it in logs, browser history, and documentation. No hidden headers to remember.
2. Easy to test
curl https://petstoreapi.com/v1/pets
curl https://petstoreapi.com/v2/pets
You can test both versions with simple HTTP requests.
3. Works with all HTTP tooling
Browsers, caches, proxies, and load balancers see different URLs. They can route, cache, and log each version independently.
4. Simple for clients
Clients just change the URL. No custom headers or content negotiation logic.
5. Easy to deprecate
You can remove /v1 endpoints without affecting /v2.
Cons
1. Not “pure” REST
REST purists argue that /v1/pets/123 and /v2/pets/123 are the same resource, so they should have the same URL. The version should be in headers or content negotiation.
2. URL pollution
Your API has multiple URL spaces: /v1/*, /v2/*, etc.
3. Harder to version individual resources
If you want to version just one endpoint, you need to version the entire API or create inconsistency.
Implementation
Major version in URL:
/v1/pets
/v2/pets
Don’t include minor versions:
❌ /v1.2/pets (too granular)
✅ /v1/pets (major version only)
Use semantic versioning internally:
- v1.0.0 - Initial release
- v1.1.0 - Add new fields (non-breaking)
- v1.2.0 - Add new endpoints (non-breaking)
- v2.0.0 - Breaking changes (new URL: /v2)
Modern PetstoreAPI uses URL versioning with /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 the same for all versions. No /v1 or /v2 prefix.
2. More “RESTful”
The resource identifier (/pets/123) doesn’t change. The representation changes based on the header.
3. Granular versioning
You can version individual resources:
GET /pets
API-Version: 2
GET /orders
API-Version: 1
Cons
1. Invisible
The version isn’t in the URL. You can’t see it in logs or browser history without checking headers.
2. Harder to test
curl -H "API-Version: 1" https://petstoreapi.com/pets
curl -H "API-Version: 2" https://petstoreapi.com/pets
You must remember to include the header.
3. Caching complexity
Caches must consider the API-Version header. You need Vary: API-Version in responses.
4. Client complexity
Clients need custom header logic. Not all HTTP clients make this easy.
5. Default version ambiguity
What happens if the client doesn’t send the header? You need a default, which creates implicit behavior.
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 consider the header when caching.
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
The version is part of the media type.
Pros
1. Most “RESTful”
This is how REST was designed. Different representations of the same resource.
2. Follows HTTP standards
Uses standard HTTP content negotiation.
3. Supports multiple formats
You can version and format simultaneously:
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 HTTP clients and tools don’t handle custom media types well.
4. Caching complexity
Caches must consider the Accept header. You need Vary: Accept.
5. Overkill for most APIs
Most APIs don’t need this level of sophistication.
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
Every response includes the API version:
X-API-Version: 1.2.0
This shows the exact version (major.minor.patch) even though the URL only shows major version.
Deprecation Warnings
When a version is deprecated, responses include:
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: <https://docs.petstoreapi.com/migration/v1-to-v2>; rel="deprecation"
Deprecation- Indicates the version is deprecatedSunset- When the version will be removedLink- 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 internally:
- Major (v1, v2) - Breaking changes, new URL
- Minor (v1.1, v1.2) - New features, backward compatible
- Patch (v1.1.1, v1.1.2) - 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 that run against 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
Test that deprecated versions include proper headers:
pm.test("Deprecated version includes headers", function() {
pm.response.to.have.header("Deprecation");
pm.response.to.have.header("Sunset");
});
Version Deprecation Strategy
How to 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 how to migrate:
Link: <https://docs.petstoreapi.com/migration/v1-to-v2>; rel="deprecation"
3. Monitor Usage
Track which clients still use deprecated versions:
X-API-Version: 1.2.0
X-Client-ID: abc123
Contact clients directly if needed.
4. Gradual Shutdown
Don’t remove the version immediately:
- Month 1-6: Announce deprecation
- Month 7-9: Add deprecation headers
- Month 10-11: Reduce rate limits for deprecated version
- Month 12: Remove deprecated version
5. Keep Documentation
Even after removal, keep documentation for the old version. Clients may need to reference it.
Conclusion
URL versioning is the most practical API versioning strategy for most teams. It’s visible, easy to test, and works with all HTTP tooling. Header versioning and content negotiation are more “pure” REST but add complexity.
Modern PetstoreAPI uses URL versioning with /v1 as the current version, semantic versioning internally, and clear deprecation policies. This approach balances pragmatism with good API design.
Use Apidog to test multiple API versions, validate version-specific behavior, and ensure smooth migrations between versions.
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 creates maintenance burden. 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 you make breaking changes. If you add new endpoints without changing existing ones, you don’t need 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, so clients don’t need to change URLs.
How do I handle version-specific bugs?
Fix bugs in all supported versions. If a bug only exists in v1, fix it in v1. Don’t force clients to upgrade to v2 for bug fixes.
Should I use semantic versioning?
Yes, internally. Track major.minor.patch versions, but only expose major versions in URLs. This gives you flexibility for non-breaking changes.
What if I need to version just one endpoint?
With URL versioning, you’d need to version the entire API or create inconsistency. This is a tradeoff. Most teams accept versioning the entire API for simplicity.



