3 min read
On this page

API Versioning & Evolution

Overview

APIs are contracts between services and their consumers. Once an API is published, changing it risks breaking every client that depends on it. API versioning and evolution strategies determine how you make changes without leaving clients behind or drowning in maintenance burden.

Why Versioning Matters

Scenario without versioning:
  1. You ship v1 of your API. 500 clients integrate.
  2. Business requirements change. You modify a response field.
  3. 500 clients break simultaneously.
  4. Your on-call phone rings continuously.

Scenario with versioning:
  1. You ship v1 of your API. 500 clients integrate.
  2. Business requirements change. You ship v2 alongside v1.
  3. Clients migrate to v2 at their own pace.
  4. You deprecate v1 after migration period.
  5. Nobody's phone rings.

URL Path Versioning

The version is part of the URL path. This is the most common approach.

https://api.example.com/v1/users/123
https://api.example.com/v2/users/123

Routing:
  /v1/* -> version 1 handler
  /v2/* -> version 2 handler

Pros:
  - Immediately visible which version is in use
  - Easy to route at the load balancer or gateway level
  - Simple to test (just change the URL)
  - Clear separation in documentation
  - Easy to run versions side by side

Cons:
  - URL changes between versions (breaks bookmarks, caches)
  - Temptation to create too many versions
  - Resource identity changes (/v1/users/123 vs /v2/users/123
    are different URLs for the same user)

Used by: Stripe (/v1/), Twitter (/2/), Google Cloud APIs

Header Versioning

The version is specified in the request header, keeping URLs clean.

GET /users/123
Accept: application/vnd.example.v2+json

or custom header:
GET /users/123
X-API-Version: 2

Pros:
  - Clean, version-free URLs
  - Resource identity stays the same across versions
  - Follows REST principle that URLs identify resources

Cons:
  - Cannot test by simply changing the URL in a browser
  - Easy to forget the header (what's the default?)
  - Harder to cache (Vary header required)
  - Less visible in logs and documentation

Used by: GitHub API (Accept header)

Query Parameter Versioning

GET /users/123?version=2
GET /users/123?api-version=2024-03-15

Pros:
  - Easy to add to existing requests
  - Visible in URLs
  - Simple for testing

Cons:
  - Pollutes the query string
  - Complicates caching (different query params = different cache keys)
  - Mixing API parameters with business parameters

Used by: Azure APIs (api-version parameter with dates)

Date-Based Versioning

Instead of numeric versions, use dates that correspond to API snapshots.

Stripe's approach:
  Stripe-Version: 2026-03-15

  Each date represents a snapshot of the API behavior.
  Your account is pinned to the version active when you created it.
  You can override per-request with the Stripe-Version header.
  Stripe maintains rolling backward compatibility.

Pros:
  - Granular versioning (each change is a version)
  - No arbitrary "v1, v2, v3" decisions
  - Clear when a version was created
  - Clients are automatically pinned to a working version

Cons:
  - More versions to maintain
  - Harder to communicate major changes
  - Requires infrastructure to support many versions simultaneously

Used by: Stripe, Twilio

Backward Compatibility

The best versioning strategy is to avoid breaking changes in the first place.

What Is Backward Compatible

Safe changes (backward compatible):
  - Adding new fields to response bodies
  - Adding new optional query parameters
  - Adding new endpoints
  - Adding new enum values (if clients handle unknown values)
  - Adding new HTTP methods to existing resources
  - Relaxing validation (accepting wider input)

Breaking changes (not backward compatible):
  - Removing fields from responses
  - Renaming fields
  - Changing field types (string to integer)
  - Changing the meaning of a field
  - Removing endpoints
  - Adding required parameters
  - Tightening validation (rejecting previously valid input)
  - Changing error formats
  - Changing authentication mechanisms

Robustness Principle

Postel's Law: "Be conservative in what you send,
be liberal in what you accept."

For API producers:
  - Always include documented fields in responses
  - Never remove fields without deprecation
  - Maintain documented behavior

For API consumers:
  - Ignore unknown fields in responses
  - Do not depend on field ordering
  - Handle new enum values gracefully
  - Use sensible defaults for missing optional fields

Additive-Only Changes

The simplest evolution strategy: only add, never remove or rename.

Original response:
  { "name": "Alice", "email": "alice@example.com" }

Need to split name into first/last:
  Bad (breaking): Replace "name" with "first_name" and "last_name"
  Good (additive): Add "first_name" and "last_name", keep "name"

  {
    "name": "Alice Smith",
    "first_name": "Alice",
    "last_name": "Smith",
    "email": "alice@example.com"
  }

  Deprecate "name" later once clients have migrated.

Deprecation Strategies

Deprecation Lifecycle

Phase 1: Announce deprecation
  - Document the deprecation in changelogs and API docs
  - Add Deprecation and Sunset HTTP headers to responses
  - Send email/notification to API consumers
  - Provide migration guide with examples

Phase 2: Warning period
  - Return deprecation warnings in response headers
  - Log usage of deprecated features for tracking
  - Offer migration tooling or codemods if possible
  - Duration: 6-12 months for public APIs

Phase 3: Sunset
  - Return 410 Gone for removed endpoints
  - Include a message pointing to the replacement
  - Duration: After warning period expires

Phase 4: Removal
  - Fully remove deprecated code
  - Monitor for any remaining traffic (alert, don't serve)

Deprecation Headers

Standard HTTP headers for deprecation:

Deprecation: true
Sunset: Sat, 01 Nov 2026 00:00:00 GMT
Link: <https://api.example.com/v2/docs>; rel="successor-version"

Response body can include warnings:
{
  "data": { ... },
  "warnings": [
    {
      "code": "DEPRECATED_FIELD",
      "message": "The 'name' field is deprecated. Use 'first_name' and 'last_name' instead.",
      "sunset_date": "2026-11-01"
    }
  ]
}

Tracking Deprecated Usage

Essential metrics for deprecation:
  - Number of unique API keys still using deprecated features
  - Request volume to deprecated endpoints over time
  - Top consumers of deprecated features (reach out directly)

Track per-feature deprecation:
  deprecated_field_usage:
    field: "name"
    daily_requests: 45,000
    unique_consumers: 23
    trend: decreasing

Do not remove a feature until usage drops to zero or
you have individually contacted remaining consumers.

API Lifecycle Management

API Maturity Stages

Experimental / Alpha:
  - May change without notice
  - No backward compatibility guarantees
  - Clearly labeled in documentation
  - Used for gathering feedback

Beta:
  - Relatively stable but may have breaking changes
  - Breaking changes announced with reasonable notice
  - Not recommended for production-critical integrations

General Availability (GA):
  - Full backward compatibility commitment
  - Breaking changes only through versioning
  - Deprecation lifecycle applies
  - SLA commitments

Deprecated:
  - Still functional but not recommended
  - Sunset date announced
  - Migration path documented

Retired:
  - No longer available
  - Returns 410 Gone or 404 Not Found

Version Maintenance Burden

The cost of supporting multiple versions:

  1 version:   Simple. One codebase, one set of tests.
  2 versions:  Manageable. Some shared code, some divergence.
  3+ versions: Painful. Bug fixes must be backported.
               Tests multiply. Documentation fragments.

Strategies to reduce burden:
  - Minimize breaking changes (prefer additive evolution)
  - Use feature flags instead of version branches
  - Share core logic, version only the API layer
  - Set maximum number of supported versions (e.g., N and N-1)
  - Aggressively sunset old versions

Real-world policies:
  Stripe: Supports all versions, converts internally
  Google: Supports major versions for 1 year after successor
  GitHub: v3 REST alongside v4 GraphQL (both maintained)

Real-World API Evolution Examples

Stripe handles API evolution through date-based versioning. Internally, they transform requests and responses between versions using a chain of version-specific transformations. A request on version 2023-01-01 passes through every transformation between that version and the latest, allowing them to maintain a single current codebase.

Slack evolved their API from simple webhook-based integrations to a full platform with events, interactivity, and Block Kit. They maintained backward compatibility by keeping old endpoints functional while introducing new patterns alongside them.

AWS uses date-based API versions and maintains backward compatibility for years. Older API versions continue to work indefinitely, though new features are only available on newer versions.

Implementation Patterns

Version Transformation Layer

Instead of maintaining separate codebases per version:

  Request -> Version Transformer -> Current Handler -> Version Transformer -> Response

  v1 request:
    { "name": "Alice Smith" }
      -> Transform: split name into first_name, last_name
    { "first_name": "Alice", "last_name": "Smith" }
      -> Current handler processes request
    { "first_name": "Alice", "last_name": "Smith", "id": 123 }
      -> Transform: combine into name field
    { "name": "Alice Smith", "first_name": "Alice", "last_name": "Smith", "id": 123 }

  This lets you maintain one handler and stack version transformations.
  Stripe uses this pattern successfully.

Feature Flags as Version Alternative

Instead of full API versions, use feature flags:

  GET /users/123
  X-Features: new-pagination,expanded-profile

  The API behavior changes based on enabled features.
  Clients opt into new behavior individually.
  No need for version numbers.

  Works well for internal APIs where you control all clients.
  Less practical for public APIs with many third-party consumers.

Common Pitfalls

  • Versioning too aggressively: Every new version is a maintenance burden. Make backward-compatible changes whenever possible to avoid new versions.
  • No deprecation timeline: Saying "this will be deprecated eventually" without a date means it never gets deprecated. Set concrete sunset dates.
  • Not tracking deprecated usage: You cannot remove a feature if you do not know who still uses it. Instrument everything.
  • Breaking changes in minor versions: If clients expect v2.1 to be compatible with v2.0, breaking that expectation erodes trust.
  • Forgetting about error format changes: Changing error response structure is a breaking change. Clients parse errors for retry logic and user messages.
  • No migration guide: A new version without a migration guide is an invitation for clients to stay on the old version forever.

Key Takeaways

  • The best version is no new version. Prefer additive, backward-compatible changes over introducing new API versions.
  • URL path versioning is the most practical approach for public APIs. Date-based versioning (Stripe's model) works well for APIs with frequent evolution.
  • Backward compatibility is a discipline: add fields, do not remove them; widen validation, do not tighten it; keep old behaviors, add new ones alongside.
  • Every deprecated feature needs a sunset date, a migration guide, and usage tracking. Without all three, deprecation does not actually happen.
  • Support at most two major versions simultaneously (current and previous). More than that multiplies testing, documentation, and bug-fix burden.
  • Design for evolution from day one: use optional fields, accept unknown properties, and version your API before the first breaking change forces you to.