TL;DR
For most teams, URL versioning (/v1/pets) is the most practical API versioning strategy. It is visible, cacheable, easy to test, and supported by standard HTTP tooling. Header versioning and content negotiation are more “pure” REST approaches, but they add operational and client-side complexity. Modern PetstoreAPI uses URL versioning with semantic versioning internally and explicit deprecation policies.
Introduction
Suppose you need to change /pets from returning a bare array to returning a wrapped object with pagination metadata:
// v1
[
{ "id": "123", "name": "Fluffy" }
]
// v2
{
"data": [
{ "id": "123", "name": "Fluffy" }
],
"pagination": {
"page": 1,
"total": 1
}
}
Existing clients may fail when the response shape changes. API versioning lets you introduce the breaking change while keeping existing integrations operational.
The main strategies are:
- URL versioning:
/v1/petsand/v2/pets - Header versioning:
API-Version: 1 - Content negotiation:
Accept: application/vnd.petstore.v1+json
For most teams, URL versioning is the best default because the version is explicit and works with browsers, caches, proxies, load balancers, and standard HTTP clients.
Modern PetstoreAPI currently exposes v1, with v2 reserved for future breaking changes.
In this guide, you’ll learn how each strategy works, how to choose one, and how to implement versioning and deprecation policies using Modern PetstoreAPI as a reference.
Why APIs Need Versioning
APIs evolve as you add features, fix bugs, and refine designs. Some changes are backward compatible; others require clients to update.
Breaking Changes
The following changes can break existing clients.
1. Removing fields
// v1
{
"id": "123",
"name": "Fluffy",
"age": 3
}
// v2 — breaking: age was removed
{
"id": "123",
"name": "Fluffy"
}
2. Changing field types
// v1
{
"price": "19.99"
}
// v2 — breaking: price changed from a string to a number
{
"price": 19.99
}
3. Changing the response structure
// v1 — bare array
[
{ "id": "123" }
]
// v2 — breaking: response is now an object
{
"data": [
{ "id": "123" }
],
"pagination": {}
}
4. Changing the URL structure
# v1
GET /pet/123
# v2 — breaking: resource name changed to plural
GET /pets/123
5. Changing authentication
# v1 — API key in the query string
GET /pets?api_key=xxx
# v2 — Bearer token
GET /pets
Authorization: Bearer xxx
Non-Breaking Changes
These changes generally do not require a new major version:
- Adding new endpoints
- Adding optional request fields
- Adding response fields, assuming clients ignore unknown fields
- Adding query parameters
- Adding new HTTP methods to existing resources
Choose a Versioning Strategy
When a breaking change is required, you have two options:
- Force every client to upgrade immediately.
- Support multiple API versions during a migration period.
Supporting multiple versions requires more maintenance, but it preserves backward compatibility and gives clients time to migrate. Most public APIs choose the second option.
URL Versioning
URL versioning places the major API version in the path:
GET /v1/pets
GET /v2/pets
The version becomes part of the resource URL, so each version can be routed, documented, monitored, and deprecated independently.
Advantages
The version is visible
You can identify the version directly in:
- Access logs
- Browser history
- Documentation
- Monitoring dashboards
- API requests
Requests are easy to test
curl https://petstoreapi.com/v1/pets
curl https://petstoreapi.com/v2/pets
No custom headers or special client configuration are required.
It works with standard HTTP infrastructure
Browsers, caches, proxies, and load balancers see different URLs for different versions. They can cache, route, and log each version independently.
Clients only need to change the URL
A client migrating from v1 to v2 can usually update its base URL:
https://petstoreapi.com/v1
becomes:
https://petstoreapi.com/v2
Deprecation is straightforward
You can deprecate /v1 while keeping /v2 available. Removing /v1 does not change the /v2 contract.
Tradeoffs
It is less “pure” REST
REST purists may argue that /v1/pets/123 and /v2/pets/123 represent the same resource and should use the same URL. In that model, the representation version belongs in a header.
It creates multiple URL spaces
Your API will contain paths such as:
/v1/pets
/v1/orders
/v2/pets
/v2/orders
Resource-level versioning is less consistent
If only one endpoint changes, you must decide whether to:
- Version the entire API
- Create an exception for that endpoint
- Use a different versioning mechanism for one resource
Most teams choose whole-API major versions to keep the contract predictable.
Implementation Guidelines
Expose only the major version in the URL:
/v1/pets
/v2/pets
Avoid exposing minor versions:
❌ /v1.2/pets
✅ /v1/pets
Track semantic versions internally:
-
v1.0.0— Initial release -
v1.1.0— Added backward-compatible fields -
v1.2.0— Added backward-compatible endpoints -
v2.0.0— Introduced breaking changes
Modern PetstoreAPI uses /v1 as its current public version.
Header Versioning
Header versioning keeps the URL stable and sends the version in a request header:
GET /pets
API-Version: 1
GET /pets
API-Version: 2
Advantages
- URLs remain clean:
/pets - The resource identifier does not change
- Individual resources can use different versions
For example:
GET /pets
API-Version: 2
GET /orders
API-Version: 1
Tradeoffs
The version is not visible in the URL
You must inspect request headers to determine which contract a client is using. The version is not obvious in browser history or basic access logs.
Requests are more verbose to test
curl \
-H "API-Version: 1" \
https://petstoreapi.com/pets
curl \
-H "API-Version: 2" \
https://petstoreapi.com/pets
Caching requires additional configuration
Caches must distinguish responses by the API-Version header. Return:
Vary: API-Version
Without the Vary header, a cache could serve a response generated for one version to a client requesting another version.
Clients need custom header logic
Every client must know how to set the version header. This is usually manageable, but it is less convenient than changing a base URL.
You need a default behavior
Decide what happens when a client omits the version:
- Reject the request
- Use the current version
- Use the oldest supported version
- Use a separately documented default
An implicit default can make migrations difficult to diagnose.
Implementation
A custom header might look like this:
API-Version: 1
You can also use the Accept header with a vendor-specific media type:
Accept: application/vnd.petstore.v1+json
If you use a custom version header, include:
Vary: API-Version
Content Negotiation
Content negotiation uses the standard Accept header to request a specific representation:
GET /pets
Accept: application/vnd.petstore.v1+json
GET /pets
Accept: application/vnd.petstore.v2+json
The version is encoded in the media type.
Advantages
- The URL remains stable
- The representation follows HTTP content-negotiation semantics
- You can version the representation and format at the same time
For example:
Accept: application/vnd.petstore.v1+json
Accept: application/vnd.petstore.v1+xml
Tradeoffs
Clients must understand media types
Client implementations need to set and parse custom media types correctly.
Requests are harder to test
curl \
-H "Accept: application/vnd.petstore.v1+json" \
https://petstoreapi.com/pets
Tooling support varies
Some HTTP clients, API tools, and documentation systems do not handle custom media types as conveniently as URL paths.
Caching requires Vary: Accept
Return:
Vary: Accept
This tells caches that the response can change based on the Accept header.
It may be unnecessary
Content negotiation is useful when representation formats are a core part of the API design. For many JSON APIs, URL versioning provides the same migration capability with less operational complexity.
Implementation
Request:
GET /pets
Accept: application/vnd.petstore.v1+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.petstore.v1+json
Vary: Accept
How Modern PetstoreAPI Implements Versioning
Modern PetstoreAPI uses URL versioning and publishes explicit version metadata and deprecation headers.
Current Version: v1
https://petstoreapi.com/v1/pets
https://petstoreapi.com/v1/orders
https://petstoreapi.com/v1/users
All current endpoints are under /v1.
Version Response Header
Responses include the exact internal API version:
X-API-Version: 1.2.0
The URL exposes the major version, while the response header communicates the major, minor, and patch 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"
These headers communicate:
-
Deprecation— The API version is deprecated. -
Sunset— The date when the version is scheduled for removal. -
Link— The migration documentation for the deprecated version.
Version Discovery
The root endpoint lists available versions:
GET https://petstoreapi.com/
{
"versions": [
{
"version": "v1",
"status": "current",
"docsUrl": "https://docs.petstoreapi.com/v1"
}
]
}
Clients can use this endpoint to discover the current version and documentation URL.
Semantic Versioning
Modern PetstoreAPI follows semantic versioning internally:
- Major (
v1,v2) — Breaking changes and a new URL path - Minor (
v1.1,v1.2) — Backward-compatible features - Patch (
v1.1.1,v1.1.2) — Backward-compatible bug fixes
Only major versions appear in URLs.
Testing API Versions with Apidog
A versioning strategy is only useful if you can verify that each version preserves its documented contract. Use separate API specifications, environments, and test suites for each version.
Import Each Version
Import the OpenAPI specification for each API version:
petstore-v1.yaml → Environment: v1
petstore-v2.yaml → Environment: v2
Keep the base URL configurable so the same test structure can run against both versions.
Run Tests Against Both Versions
Create requests for each base URL:
// 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");
The important part is to run equivalent scenarios against both versions, including:
- Successful reads and writes
- Validation errors
- Authentication failures
- Pagination
- Empty results
- Resource-not-found responses
Validate Version-Specific Behavior
If v1 returns a bare array and v2 returns a wrapped object, assert those contracts explicitly:
// v1 returns a bare array
pm.test("v1 returns an array", function () {
pm.expect(pm.response.json()).to.be.an("array");
});
// v2 returns a wrapped object
pm.test("v2 returns data and pagination", function () {
const body = pm.response.json();
pm.expect(body).to.have.property("data");
pm.expect(body).to.have.property("pagination");
});
These tests prevent an implementation from accidentally returning the v2 response shape from a v1 endpoint.
Check Deprecation Headers
For deprecated versions, verify that the required headers are present:
pm.test("Deprecated version includes deprecation headers", function () {
pm.response.to.have.header("Deprecation");
pm.response.to.have.header("Sunset");
});
You can also validate the migration link and confirm that the Sunset date is formatted as expected.
Version Deprecation Strategy
A deprecation policy should give clients enough time to migrate without leaving old versions active indefinitely.
1. Announce Deprecation Early
Give clients at least 6–12 months of notice when possible:
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Publish the announcement in your API documentation and notify known consumers directly.
2. Provide a Migration Guide
Document every breaking change and show the required client updates:
Link: <https://docs.petstoreapi.com/migration/v1-to-v2>; rel="deprecation"
A useful migration guide should include:
- Endpoint changes
- Request changes
- Response-shape changes
- Authentication changes
- Error behavior changes
- Before-and-after examples
- A target migration date
3. Monitor Usage
Track which clients still call the deprecated version:
X-API-Version: 1.2.0
X-Client-ID: abc123
Use access logs, metrics, or request tracing to identify active consumers. Contact clients that have not migrated before the sunset date.
4. Shut Down the Version Gradually
A sample timeline is:
- Months 1–6: Announce deprecation and publish the migration guide
- Months 7–9: Add deprecation and sunset headers
- Months 10–11: Send final migration reminders and review usage
- Month 12: Remove the deprecated version
Avoid removing a version without checking whether active clients still depend on it.
5. Keep Historical Documentation
Keep documentation for removed versions available when possible. Clients may need it to understand old payloads, troubleshoot integrations, or complete an internal migration.
Versioning is one of several naming decisions that shape a REST API’s long-term maintainability. Whether resource names should be plural or singular is another convention worth settling before your first public release.
Conclusion
URL versioning is the most practical default for most teams. It is visible, easy to test, and compatible with standard HTTP tooling. Header versioning and content negotiation are valid alternatives, but they require more client, caching, and operational configuration.
Modern PetstoreAPI uses:
- URL versioning with
/v1as the current version - Semantic versioning internally
-
X-API-Versionresponse headers - Deprecation and sunset headers
- A version discovery endpoint
- Migration documentation for deprecated versions
Use Apidog to import each API specification, run tests against multiple versions, validate version-specific response contracts, and verify that deprecation headers are returned correctly.
FAQ
Should I use URL versioning or header versioning?
Use URL versioning unless you have a specific reason not to. It is easier to understand, test, route, cache, and document. Header versioning can provide cleaner URLs, but it adds complexity that many APIs do not need.
How many versions should I support simultaneously?
Support two versions at most: the current version and the previous version. Supporting more versions increases implementation and testing costs. Give clients 6–12 months to migrate, then remove the old version according to your deprecation policy.
Should I version from v0 or v1?
Start with v1 for a public API. v0 often signals instability. If the contract is not stable enough for a first public release, continue iterating before publishing it.
Do I need to version every endpoint?
No. Create a new major version when you introduce breaking changes. Adding new endpoints without changing existing contracts does not require a new version.
With URL versioning, teams commonly expose the entire API under a new major path to keep routing and documentation consistent.
Should I include minor versions in URLs?
No. Prefer:
/v1/pets
over:
/v1.2/pets
Minor and patch releases should remain backward compatible, so clients should not need to change their URLs.
How do I handle version-specific bugs?
Fix bugs in every supported version where the bug exists. Do not force clients to upgrade to a new major version just to receive a bug fix.
Should I use semantic versioning?
Yes, internally. Track major, minor, and patch versions, but expose only major versions in URL paths. This lets you release backward-compatible features and fixes without creating unnecessary URL versions.
What if I need to version just one endpoint?
With URL versioning, you can either version the entire API or introduce an exception for that endpoint. The latter can make the API inconsistent, so most teams accept versioning the broader API for simplicity.
Top comments (0)