• 1. URI-based versioning (e.g., /v1/users) is clearer than header-based versioning
  • 2. Additive changes (new fields, endpoints) don’t require version bumps
  • 3. Breaking changes (removing fields, changing types) require new major versions
  • 4. Deprecation policies should give clients 12-18 months to migrate

The Versioning Dilemma

After managing 150+ production APIs, we’ve seen every versioning strategy. Header-based, query parameter-based, URI-based, content negotiation. They all work, but URI-based versioning wins for simplicity and clarity.

/v1/users vs /v2/users is immediately obvious. No guessing about headers. Works with any HTTP client. Easy to cache and route.

Breaking vs Additive Changes

Not every change requires a version bump. Understanding the difference between breaking and additive changes is critical to maintaining backward compatibility.

Additive Changes (No Version Bump)

  • 1. Adding new endpoints: /v1/orders/export
  • 2. Adding optional request fields: filters?: string[]
  • 3. Adding new response fields (clients ignore unknown fields)
  • 4. Making required fields optional

Breaking Changes (Require New Version)

  • 1. Removing endpoints or fields
  • 2. Changing field types: age: string → number
  • 3. Making optional fields required
  • 4. Changing authentication schemes
  • 5. Altering HTTP status codes for existing errors

Deprecation Strategy

  • 1. +0 months: Release v2, announce v1 deprecation
  • 2. +3 months: Add Sunset header to v1 responses
  • 3. +6 months: Email all v1 clients with migration guide
  • 4. +12 months: Final warning, set shutdown date
  • 5. +18 months: Decommission v1

Version Discovery

Make it easy for clients to discover supported versions. Add a /versions endpoint:

GET /versions

{
  "versions": [
    {
      "version": "v2",
      "status": "current",
      "released": "2024-01-15"
    },
    {
      "version": "v1",
      "status": "deprecated",
      "sunset": "2025-07-15",
      "released": "2023-01-10"
    }
  ]
}

Common Pitfalls

  1. Micro-Versioning: Don’t create v1.1, v1.2, v1.3. Stick to major versions only for simplicity.
  2. Silent Breaking Changes: Always document what changed between versions in your changelog.
  3. No Migration Path: Provide migration guides, code examples, and automated tools when possible.

Supporting Multiple Versions

How do you maintain v1 and v2 simultaneously without duplicating all your business logic? We use a versioned adapter pattern: shared domain logic with version-specific DTOs and transformers.

The controller routes to different adapter classes based on version. Each adapter transforms the shared domain model to its version-specific response format.