1. Library
  2. Http and the Web
  3. Apis

Updated 10 hours ago

API versioning exists because APIs are promises.

When a developer integrates with your API, they're trusting that their code will keep working. They're building their product on your foundation. Breaking that foundation breaks their product—and their trust.

Versioning is how you evolve without betraying that trust.

What Breaks and What Doesn't

Not every change requires a new version. Understanding the difference saves you from version proliferation and saves your users from migration fatigue.

Breaking changes violate existing expectations:

  • Removing an endpoint or field
  • Changing a field's type or meaning
  • Adding a required parameter
  • Changing error codes or response structures

Non-breaking changes extend without violating:

  • Adding new endpoints
  • Adding optional parameters
  • Adding new fields to responses (clients should ignore unknown fields)
  • Fixing bugs to match documented behavior

The discipline is recognizing that "improvement" and "breaking change" often travel together. You want to fix that poorly-named field? That's a breaking change. You want to restructure that response for consistency? Breaking change. Every improvement that touches existing behavior forces a choice: is this worth a broken promise?

The Versioning Strategies

URI versioning is the most common:

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

The version is visible, explicit, impossible to miss. Developers see it in every URL. The tradeoff: URLs proliferate, and HTTP caching treats each version as entirely different resources.

Header versioning keeps URLs clean:

GET /users
API-Version: 2

Or using content negotiation:

GET /users
Accept: application/vnd.myapi.v2+json

This is more "RESTful"—using HTTP's built-in mechanisms. But it's less discoverable. The version hides in headers, invisible in browser address bars and documentation examples. Testing becomes harder when version isn't in the URL.

Query parameter versioning exists but feels wrong:

https://api.example.com/users?version=2

Version isn't really a query parameter—it's fundamental to what you're asking for. This approach works, but it's easy to omit accidentally, defaulting to some version unexpectedly.

No versioning is a valid choice. Some APIs commit to never making breaking changes—only additive, backward-compatible evolution forever. GraphQL APIs often take this path. It's harder than it sounds, but it's honest: instead of managing version promises, you make one promise and keep it.

Most APIs choose URI versioning. It's explicit, discoverable, and matches how developers think about APIs. The theoretical purity of header versioning rarely outweighs the practical clarity of /v1/ in the URL.

The Version Lifecycle

Versions have a lifecycle, and managing it well is the difference between a trusted API and an abandoned one.

Introduction: The new version launches. Document what changed, what's new, and how to migrate from the previous version. Be specific—developers need to know exactly what breaks.

Adoption: Both versions run simultaneously. Clients migrate at their own pace. This is where you learn whether your migration guide is actually good.

Deprecation: The old version is marked deprecated. This isn't sunset—it still works. But responses include deprecation warnings (in headers or metadata), documentation shows warnings, and you communicate directly with developers still using it.

The deprecation period matters enormously. For public APIs, 6-12 months is reasonable. Shorter periods anger developers who planned their roadmaps around your stability. Longer periods multiply your maintenance burden.

Sunset: The old version dies. On the sunset date, requests return 410 Gone or redirect to the new version. By this point, anyone still using the old version has had ample warning.

Every breaking change is a broken promise—and promises compound. Break enough of them, and developers stop trusting your API entirely. They'll look for alternatives that respect their time.

Supporting Multiple Versions

Running multiple versions simultaneously is expensive. Every version is code to maintain, bugs to fix, security patches to apply.

The simplest approach is code separation: v1 handlers live in one place, v2 handlers in another. Clean boundaries, but duplication. When you fix a bug in shared logic, you fix it in both places.

The alternative is shared code with version-specific transformations. Core business logic stays unified. Adapters or serializers transform data into version-specific response formats. This reduces duplication but scatters version awareness throughout the codebase.

Most APIs land somewhere between: route requests to version-specific handlers, share business logic, use adapters for the parts that actually differ between versions.

The goal is minimizing the surface area of version-specific code while keeping version boundaries clear.

When to Create a New Version

Not every breaking change deserves a new version. Versions have overhead—migration effort for developers, maintenance burden for you, complexity for everyone.

Batch breaking changes together. If you're creating v2 anyway, bundle the other breaking changes you've been wanting to make. Releasing v1, v2, v3 in rapid succession creates migration fatigue.

Consider alternatives. Can you add a new endpoint instead of changing an existing one? Can you support both old and new formats temporarily with feature flags?

Evaluate impact. How many clients use this? How hard is migration? Is the improvement worth the disruption?

Reserve major versions for changes significant enough to justify the cost. A renamed field probably isn't. A restructured data model probably is.

Internal vs. Public APIs

Internal APIs—used only by your own applications—can version aggressively. You control all the clients. Migration can be coordinated. You might not need versioning at all if you can update clients and servers together.

Public APIs require conservatism. External developers built products on your promises. They have roadmaps, release cycles, resource constraints. Breaking their code costs them money and trust.

The more public your API, the longer your deprecation periods, the clearer your communication, the more generous your support windows.

Documentation That Respects Developers

Version documentation isn't optional—it's how you honor the trust developers place in your API.

Changelogs list exactly what changed: added, deprecated, removed, modified. Not marketing language—technical precision.

Migration guides show the path from old to new. Code examples. Common pitfalls. The questions developers will ask before they ask them.

Version-specific documentation ensures developers see docs for their version, not a confusing mix of features that may or may not exist in the version they're using.

Deprecation notices appear everywhere relevant: documentation, API responses (via headers), direct communication to affected developers.

Common Mistakes

Versioning too frequently. v1, v2, v3 in six months exhausts developers. Each version is migration work they didn't ask for.

Inconsistent versioning. /v1/users and /api-version-2/posts in the same API signals chaos.

Insufficient deprecation periods. 30 days notice for a major version sunset is disrespectful. Developers have their own schedules.

No migration path. "Here's v2" without explaining how to get there leaves developers stranded.

Breaking changes without version bumps. Changing v1 behavior silently is the worst broken promise—the one developers don't even know about until their code fails.

Version everywhere. /v1/api/v1/users/v1/profile is absurd. Version once at the API level.

Frequently Asked Questions About API Versioning

Was this page helpful?

😔
🤨
😃