API Versioning Strategies
You ship an API. People start using it. Then you need to make a change that would break their code. What do you do?
This is the API versioning problem, and every team faces it eventually. The decisions you make here affect your users and your ability to iterate. Let's look at your options.
Why Version at All?
Some teams try to avoid versioning by never making breaking changes. They only add new fields, new endpoints, new parameters. Old clients continue working forever.
This works until it doesn't. Sometimes you need to rename a field that was poorly named. Sometimes you need to change a data type. Sometimes your entire data model needs restructuring.
Without versioning, you're trapped by every past mistake. Versioning gives you a path forward.
What Counts as a Breaking Change?
Before discussing how to version, you need to know when to version. Breaking changes include:
- Removing or renaming fields
- Changing field types
- Changing the meaning of a value
- Removing endpoints
- Changing required parameters
- Changing authentication requirements
Non-breaking changes you can make without versioning:
- Adding new optional fields
- Adding new endpoints
- Adding new optional parameters
- Relaxing validation
The key insight: clients should be able to ignore things they don't understand. If they parse only the fields they need, new fields don't break them.
URL Path Versioning
The most common approach: put the version in the URL.
/api/v1/users
/api/v2/users
Pros:
- Obvious and explicit
- Easy to test and debug
- Easy to route in your infrastructure
- Clients can't accidentally hit the wrong version
Cons:
- URLs aren't supposed to change when the resource is the same
- Caching can be complicated
- Feels inelegant to some people
Despite the theoretical objections, URL versioning is practical and widely understood. When in doubt, use this.
Header Versioning
Put the version in a request header:
Accept: application/vnd.myapi.v2+json
Or a custom header:
API-Version: 2
Pros:
- URLs stay clean
- More "correct" from a REST perspective
- Can version the media type separately from the API
Cons:
- Harder to test in a browser
- Easy for clients to forget the header
- Less visible when debugging
Header versioning works fine but adds friction. Most developers find URL versioning easier to work with.
Query Parameter Versioning
/api/users?version=2
Similar to URL versioning but keeps the path clean. The downside is it's easy to forget the parameter, and you need to decide what happens when it's missing.
This approach is less common and can feel inconsistent. I'd pick URL or header versioning instead.
No Explicit Versioning
Some APIs version implicitly through different field names or by allowing clients to specify which fields they want. GraphQL takes this approach, letting clients query exactly what they need.
This can work well but requires careful API design from the start. You can deprecate fields while keeping them available, giving clients time to migrate.
How Many Versions to Support
Supporting multiple versions costs engineering time. Every bug fix might need to be applied to multiple versions. Documentation multiplies. Testing multiplies.
Common approaches:
Support N versions: Keep the last 2-3 major versions. Deprecate older ones with clear timelines.
Time-based: Support each version for a fixed period, like 18 months after the next version releases.
Usage-based: Monitor which versions are actually used and sunset ones with minimal traffic.
Whatever you choose, communicate clearly. Give users notice before deprecating versions. Provide migration guides.
Deprecation Done Right
When deprecating, follow these steps:
- Announce the deprecation with a clear timeline
- Add deprecation headers to API responses
- Document migration path to the new version
- Monitor usage of deprecated versions
- Reach out to heavy users directly if needed
- Finally, remove the old version
Don't just flip a switch and break people. Even if you technically can, you'll damage trust. Give reasonable time and support for migration.
Implementation Strategies
How do you actually implement multiple versions in your codebase?
Separate controllers/handlers: Each version has its own code path. Clear but can lead to duplication.
Version adapters: One core implementation with adapters that transform requests and responses for each version. Less duplication but more complexity.
Feature flags: Use flags to enable/disable features based on version. Can get messy with many version differences.
For small APIs with few differences between versions, separate handlers are fine. For complex APIs with many versions, adapters scale better.
Semantic Versioning for APIs
Consider using semantic versioning principles:
- Major version: breaking changes
- Minor version: new features, backwards compatible
- Patch version: bug fixes
You might only expose major versions in your API paths (/v1, /v2) while tracking minor and patch versions internally.
Document Everything
Every version needs clear documentation:
- What's different from the previous version
- Migration guide for upgrading
- Deprecation timeline if applicable
- Complete API reference for that version
Good documentation makes versioning painless for users. Bad documentation makes every version change painful.
Start with v1
Even if you're not sure you'll ever need versions, start with /api/v1. It costs nothing and gives you an escape hatch. Starting with an unversioned API and adding versioning later is awkward.
Your future self will thank you for leaving room to evolve.