REST API Versioning: URL vs Header—How to Choose and Implement Well

A practical guide comparing REST API versioning via URL paths versus headers, with pros, cons, caching, tooling, and a clear decision framework.

ASOasis
7 min read
REST API Versioning: URL vs Header—How to Choose and Implement Well

Image used for representation purposes only.

Why API versioning matters

APIs are living systems. You fix bugs, add fields, retire unsafe patterns, and sometimes make breaking changes. Versioning gives you a contract to do that safely without surprising clients in production. The practical question isn’t “Should I version?”—it’s “Where should the version live?” For REST-style APIs, the two most common approaches are:

  • URL path versioning: https://api.example.com/v1/orders
  • Header-based versioning: Accept: application/vnd.example.orders+json; version=2 or X-API-Version: 2024-06-01

This article compares both, shows concrete examples, and offers a decision framework you can apply today.

What counts as a “version”?

Before picking a location, decide what a version represents.

  • Breaking changes: Remove/rename fields, change types, alter semantics, or status codes. These require a new major version.
  • Backwards-compatible changes: Add optional fields, new endpoints, or expand enum values. These should not require a major bump.
  • Representation vs. resource: With REST, a URI identifies a resource; a “version” often describes the representation of that resource. Keeping this distinction in mind helps when evaluating header-based content negotiation.

A simple policy that works well in practice:

  • Major versions for breaking changes.
  • Minor/patch changes are additive and safe; avoid forcing new versions for these.

URL path versioning

With path versioning, the version is part of the route.

Example:

curl -H "Authorization: Bearer <token>" \
  https://api.example.com/v2/orders/123

Advantages

  • Clear and explicit: Easy for developers, logs, and dashboards to understand at a glance.
  • Simple routing: Gateways, load balancers, and service meshes can route by prefix (/v1, /v2) without parsing headers.
  • CDN-friendly caching: Versioned paths are effectively immutable and play nicely with long-lived caches and cache-busting.
  • Documentation and SDKs: Many OpenAPI toolchains assume path-based versioning, simplifying client generation.

Drawbacks

  • Resource identity drift: If the URI changes between versions, links and bookmarks no longer point to a single canonical resource.
  • URL churn in clients: Upgrading requires swapping base paths throughout code and configuration.
  • HATEOAS friction: Embedding versioned links can lock representations to a specific major.

When it fits best

  • Public, widely cached APIs that benefit from immutable URIs and simple CDN keys.
  • Polyglot client ecosystems where explicitness and ease of routing outweigh elegance.

Header-based versioning

With header versioning, clients request a representation via headers. There are two common patterns:

  1. Vendor media types (content negotiation):
curl https://api.example.com/orders/123 \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/vnd.example.orders+json; version=2"
  1. Explicit version header:
curl https://api.example.com/orders/123 \
  -H "Authorization: Bearer <token>" \
  -H "X-API-Version: 2024-06-01"

Advantages

  • Stable URIs: The resource location remains constant across versions; only the representation varies.
  • Aligns with HTTP semantics: Content negotiation is an intended use of the Accept header.
  • Smooth upgrades: Clients can pin versions per request and selectively migrate endpoints.
  • Hypermedia friendly: Links can remain version-agnostic.

Drawbacks

  • Caching complexity: CDNs and proxies must key on headers (Vary: Accept, X-API-Version). Misconfiguration can lead to cache poisoning or low hit rates.
  • Operational visibility: Logs and dashboards need header extraction to segment by version.
  • Tooling variance: Some generators and API explorers expect path versions; header-based flows may require extra documentation.

When it fits best

  • APIs prioritizing stable resource identifiers and hypermedia.
  • Private/B2B APIs where clients can handle header conventions and you control caching infrastructure.

Caching, CDNs, and correctness

Caching is often the deciding factor.

  • Path versioning
    • Cache keys: The path alone differentiates versions.
    • Strategy: Serve immutable artifacts with long TTLs; use Cache-Control: public, max-age=31536000, immutable and ETag per version.
  • Header versioning
    • Vary headers: Always return Vary: Accept (and Vary: X-API-Version if you use a custom header). This prevents content-mismatch.
    • CDN configuration: Ensure the CDN includes these headers in the cache key. Test hit rates and correctness under load.
    • Selective TTLs: You can cache v1 and v2 differently without duplicating paths.

Routing, gateways, and observability

  • Routing
    • Path: Route by prefix; trivial in NGINX, Kong, Envoy, API Gateway.
    • Header: Route by header match rules. Slightly more complex but expressive.
  • Observability
    • Path: Metrics, traces, and logs naturally segment by version.
    • Header: Add log enrichment (e.g., structured field api_version) and expose per-version dashboards.
  • Rate limiting
    • Consider per-version quotas to prevent noisy-neighbor effects during migrations.

Status codes and negotiation behavior

Regardless of approach, be explicit about server behavior.

  • Unsupported version
    • Path: 404 Not Found or 410 Gone for retired routes.
    • Header: 406 Not Acceptable (unknown Accept) or 415 Unsupported Media Type (if you use Content-Type negotiation).
  • Deprecation and retirement
    • Return Deprecation and/or Sunset response headers when applicable, plus a Link header to documentation.
    • Communicate final removal dates in responses and out-of-band channels.

Documentation and OpenAPI

  • Keep a separate OpenAPI document per major version. This avoids cross-version ambiguity.
  • For header-based versions, document:
    • Required Accept or X-API-Version formats.
    • Error semantics on negotiation failure.
  • Generate language SDKs per major. Let minor/patch evolve without breaking changes.

Security and compatibility

  • OAuth scopes: Keep scopes stable across minor versions. Introduce new scopes only with clear migration notes.
  • PII and compliance: Guard new fields behind explicit opt-in versions to avoid accidental exposure.
  • Idempotency: Preserve method semantics across versions; don’t silently change POST vs. PUT behavior.

A pragmatic decision framework

Use these bullets as a quick checklist.

Choose URL path versioning if:

  • You need maximum CDN friendliness and simple cache keys.
  • You want trivial routing and out-of-the-box compatibility with gateways and tools.
  • Your clients prefer explicit, easy-to-see versioning in the endpoint.

Choose header-based versioning if:

  • Stable URIs and clean hypermedia are important design goals.
  • You can reliably configure caches and proxies with Vary.
  • You want per-request flexibility to pin versions without changing client base URLs.

Consider a hybrid:

  • Major in path, minor in header. Example: /v2 plus Accept with version=2.3 for representational tweaks that remain backwards-compatible.
  • Keep the path major stable for years; use header parameters for additive changes and previews.

Migration playbook (example)

  1. Announce and document
  • Publish a clear changelog explaining what breaks and why.
  • Provide side-by-side examples v1 vs. v2.
  1. Dual-run
  • Operate v1 and v2 simultaneously for a defined window.
  • Mirror production traffic in staging to validate v2 correctness.
  1. Instrumentation
  • Add per-version metrics, error rates, and latency SLOs.
  • Track client adoption; identify laggards early.
  1. Deprecation headers
  1. Enforcement ramp
  • Introduce warnings, then soft blocks for non-critical endpoints.
  • After the Sunset date, respond with 410 Gone on v1 and provide remediation guidance.

Concrete implementation tips

  • Content negotiation (header-based)

    • Accept: application/vnd.example.resource+json; version=2
    • Always echo Content-Type with the negotiated media type and version.
    • Return Vary: Accept (and Vary: X-API-Version if used).
  • Path-based routing (NGINX snippet)

location ~ ^/v2/ {
  proxy_pass http://orders-v2;
}
location ~ ^/v1/ {
  proxy_pass http://orders-v1;
}
  • ETags and caches

    • Compute ETag per version and representation.
    • For immutable versioned paths, consider Cache-Control: public, max-age=31536000, immutable.
  • Testing

    • Add contract tests that validate both version negotiation and payload shape.
    • Run consumer-driven contract tests from key clients before release.

Common pitfalls to avoid

  • Query parameter versioning: Harder for caches and tooling; generally avoid (?version=2).
  • Silent defaults: Requiring no version and changing behavior under clients is dangerous. Force explicit versioning for breaking changes.
  • Version sprawl: Keep the number of supported majors small. Retire old versions with a predictable policy.
  • Field-level version flags: They increase server complexity and surprise clients. Prefer clean, versioned representations.

Worked comparison (at a glance, no table)

  • URL path
    • Pros: Simple, CDN-friendly, obvious in logs, tooling support.
    • Cons: Changing URIs, less RESTful purity, clients must refactor base URLs.
  • Header
    • Pros: Stable URIs, leverages HTTP negotiation, granular upgrades.
    • Cons: Requires correct Vary/caching setup, more complex observability and tooling.

Final guidance

If you run a public API with large-scale caching and a diverse client base, path-based versioning is typically the pragmatic default. If you optimize for canonical resource identifiers, hypermedia, and fine-grained client control, header-based versioning shines—provided your platform and CDN are configured to respect negotiation headers. A hybrid model—major in the path, representational details in headers—often provides the best of both worlds.

Pick one, document it relentlessly, and back it with a strong compatibility and deprecation policy. That consistency, more than the mechanism itself, is what keeps integrations stable for years.

Related Posts