DEV Community

Cover image for What Is the Best API Versioning Strategy: URL, Header, or Content Negotiation?
Wanda
Wanda

Posted on • Originally published at apidog.com

What Is the Best API Versioning Strategy: URL, Header, or Content Negotiation?

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.

Try Apidog today

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"}
Enter fullscreen mode Exit fullscreen mode

2. Changing field types:

// v1
{"price": "19.99"}

// v2 (breaking: string to number)
{"price": 19.99}
Enter fullscreen mode Exit fullscreen mode

3. Changing response structure:

// v1 (bare array)
[{"id": "123"}]

// v2 (breaking: wrapped object)
{"data": [{"id": "123"}], "pagination": {...}}
Enter fullscreen mode Exit fullscreen mode

4. Changing URL structure:

// v1
GET /pet/123

// v2 (breaking: plural)
GET /pets/123
Enter fullscreen mode Exit fullscreen mode

5. Changing authentication:

// v1: API key in query
GET /pets?api_key=xxx

// v2 (breaking: Bearer token)
GET /pets
Authorization: Bearer xxx
Enter fullscreen mode Exit fullscreen mode

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:

  1. Force all clients to upgrade – Simple, but breaks integrations.
  2. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Avoid minor versions in URLs:

❌ /v1.2/pets  (too granular)
✅ /v1/pets    (major only)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Or use Accept header:

Accept: application/vnd.petstore.v1+json
Enter fullscreen mode Exit fullscreen mode

Include Vary header:

Vary: API-Version
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Response:

Content-Type: application/vnd.petstore.v1+json
Vary: Accept
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

All endpoints are under /v1.

Version Response Header

All responses include the API version:

X-API-Version: 1.2.0
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode
  • 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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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");
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

2. Provide Migration Guide

Document all breaking changes and migration steps:

Link: <https://docs.petstoreapi.com/migration/v1-to-v2>; rel="deprecation"
Enter fullscreen mode Exit fullscreen mode

3. Monitor Usage

Track which clients use deprecated versions:

X-API-Version: 1.2.0
X-Client-ID: abc123
Enter fullscreen mode Exit fullscreen mode

Contact clients if needed.

4. Gradual Shutdown

Recommended timeline:

  1. Months 1–6: Announce deprecation
  2. Months 7–9: Add deprecation headers
  3. Months 10–11: Reduce rate limits for deprecated version
  4. 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)