Skip to main content
API Design Mastery

Versioning Strategies

Ravinder··5 min read
API DesignRESTGraphQLgRPCVersioning
Share:
Versioning Strategies

Every API will eventually need to change in a breaking way. The question is not whether, but when — and whether your versioning strategy gives clients enough runway to adapt without firefighting on both ends. Most teams reach for URL versioning by default, ship it, and later discover its hidden costs. Understanding all three approaches before you commit can save months of pain.

What Counts as a Breaking Change

Before picking a strategy, agree on what triggers a new version. A non-exhaustive list:

  • Removing or renaming a field
  • Changing a field's type (string → integer)
  • Making an optional field required
  • Removing an endpoint
  • Changing authentication scheme
  • Altering error response shape

Additive changes — new optional fields, new endpoints, new enum values your clients are told to ignore — are generally safe without a version bump.

flowchart LR A[API Change] --> B{Breaking?} B -- No --> C[Ship without version bump] B -- Yes --> D{Strategy?} D --> E[URL: /v2/...] D --> F[Header: API-Version: 2025-10-01] D --> G[Content Type: application/vnd.api+json;v=2]

URL Versioning

The most visible and most widely adopted approach. Version lives in the path:

GET /v1/orders/42
GET /v2/orders/42

Pros: trivially cacheable, easy to test in a browser, obvious in logs.

Cons: the URL should identify a resource, not an API contract epoch. You end up with two URLs pointing at the same logical resource. Documentation, links, and bookmarks silently rot when you retire /v1.

GET /v2/orders/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJ...

URL versioning works well when your clients are third-party developers who paste URLs into config files. It fails when you have many internal services calling each other, because every service must be updated when you retire a version.

Header Versioning

Version travels in a request header, leaving the URL stable:

GET /orders/42 HTTP/1.1
Host: api.example.com
API-Version: 2025-10-01
Authorization: Bearer eyJ...

Date-based versions (used by Stripe, Twilio) are more expressive than integers: 2025-10-01 tells you exactly when the contract was introduced. Each client is pinned to the version active when it was written; you can advance their pin deliberately during upgrades.

Pros: resource identity is stable, date-based versions carry semantic meaning, easy to add version pinning per API key.

Cons: harder to test with curl by default, caching requires Vary: API-Version on CDN/proxy layers.

HTTP/1.1 200 OK
Content-Type: application/json
API-Version: 2025-10-01
Sunset: Sat, 01 Apr 2026 00:00:00 GMT

Content Negotiation

Version is embedded in the Accept media type, following RFC 6838:

GET /orders/42 HTTP/1.1
Accept: application/vnd.example.order+json;version=2

This is the most "correct" from a REST purist's perspective — resources are negotiated by representation type, which is exactly what Accept is for.

Pros: extremely precise, lets one endpoint serve multiple representation formats simultaneously.

Cons: verbose, most HTTP clients handle this poorly out of the box, middleware and API gateways rarely index on Accept for routing, and debugging requires inspecting headers carefully.

Few public APIs use this successfully. It shines in internal hypermedia APIs and document-oriented systems.

Choosing a Strategy

Factor URL Header Content Negotiation
Third-party developers Excellent Good Poor
Internal microservices Acceptable Excellent Acceptable
CDN cacheability Excellent Requires Vary Requires Vary
Browser testable Yes No No
Resource URL stability Poor Excellent Excellent

A practical recommendation: use URL versioning for your public API (developer experience wins), and date-based header versioning for internal APIs where URL stability matters and you control all clients.

Running Two Versions Simultaneously

Regardless of strategy, you will run multiple versions in parallel. Keep that window as short as possible.

gantt title API Version Lifecycle dateFormat YYYY-MM-DD section v1 Active :a1, 2024-01-01, 2025-04-01 Deprecated :crit, a2, 2025-04-01, 2025-10-01 Sunset :milestone, 2025-10-01, 0d section v2 Active :a3, 2025-01-01, 2026-06-01

Route at the gateway level, not in application code:

# nginx example
location /v1/ {
  proxy_pass http://api-v1-service/;
}
location /v2/ {
  proxy_pass http://api-v2-service/;
}

This keeps version branching out of your business logic and lets you kill v1 by updating a config file.

Deprecation Signaling

Send signals early and repeatedly:

HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 01 Apr 2026 00:00:00 GMT
Link: <https://api.example.com/v2/orders/42>; rel="successor-version"

The Sunset header (RFC 8594) and Deprecation header (draft-ietf-httpapi-deprecation-header) are standard. Log usage of deprecated endpoints per client ID so you can reach out to stragglers before the sunset date.

Key Takeaways

  • Define what counts as a breaking change before your first public release; ambiguity creates conflict later.
  • URL versioning wins on developer experience and CDN compatibility; use it for public APIs.
  • Date-based header versioning is more expressive and keeps resource URLs stable; use it for internal service APIs.
  • Content negotiation is theoretically correct but practically painful for most teams.
  • Run v1 and v2 in parallel at the gateway layer, not inside application code.
  • Send Sunset and Deprecation headers from the moment you ship a new version, and monitor per-client usage of old endpoints to manage the rolloff actively.
Share: