API Versioning: Evolving Your API Without Breaking Clients
You rename a field in your API response. user_name becomes username. You deploy. Half your clients break. They were parsing user_name and now get undefined. You roll back. You spend a week coordinating with every client team to update their code simultaneously. You deploy again. One team missed the memo. They break again.
This is why API versioning exists. It lets you evolve your API while giving clients time to migrate.
What API versioning actually solves
API versioning is not about having multiple versions forever. It is about managing the transition period between versions. The goal is to:
- Make breaking changes without immediately breaking clients
- Give clients a clear migration path and timeline
- Eventually deprecate and remove old versions
A breaking change is anything that causes existing clients to fail:
- Removing a field
- Renaming a field
- Changing a field’s type
- Changing the meaning of a field
- Removing an endpoint
- Changing required parameters
A non-breaking change is safe to deploy without versioning:
- Adding a new optional field
- Adding a new endpoint
- Adding a new optional parameter
- Relaxing validation rules
Versioning strategies
URL versioning
Include the version in the URL path: /v1/users, /v2/users.
Pros:
- Explicit and visible
- Easy to route at the load balancer or API gateway
- Easy to test (just change the URL)
- Works with any HTTP client
Cons:
- URLs are supposed to identify resources, not API versions
- Clients must update URLs when migrating
- Can lead to URL proliferation
Used by: Stripe (/v1/), Twilio, GitHub (/v3/), most public APIs.
Header versioning
Include the version in a request header: API-Version: 2024-01-15 or Accept: application/vnd.api+json;version=2.
Pros:
- Cleaner URLs
- More RESTful (URLs identify resources, headers describe how to process them)
- Easier to add versioning to existing APIs without changing URLs
Cons:
- Less visible (easy to forget to set the header)
- Harder to test in a browser
- Caching is more complex (CDNs cache by URL by default, not headers)
Used by: Stripe also supports Stripe-Version header, GitHub API v4 (GraphQL).
Query parameter versioning
Include the version as a query parameter: /users?version=2 or /users?api_version=2024-01-15.
Pros: Easy to add to existing APIs. Visible in URLs.
Cons: Query parameters are for filtering/sorting, not versioning. Pollutes the query string.
Rarely used in production APIs.
Date-based versioning
Use a date instead of a number: API-Version: 2024-01-15. Stripe uses this model.
Pros: Clear when the version was introduced. Clients can see exactly what behavior they are locked to.
Cons: More verbose than v1, v2. Requires documentation of what changed on each date.
graph LR subgraph strategies["Versioning Strategies"] URL["URL Versioning /v1/users /v2/users"] HDR["Header Versioning API-Version: 2024-01-15"] QP["Query Parameter /users?version=2"] end subgraph tradeoffs["Tradeoffs"] T1["Explicit, easy to route URL proliferation"] T2["Clean URLs, RESTful Less visible, cache complexity"] T3["Easy to add Pollutes query string"] end URL --- T1 HDR --- T2 QP --- T3 style URL fill:#E1F5EE,stroke:#0F6E56,color:#085041 style HDR fill:#EEEDFE,stroke:#534AB7,color:#3C3489 style QP fill:#F1EFE8,stroke:#888780,color:#444441
Backward compatibility: the real goal
The best versioning strategy is to avoid breaking changes in the first place. Backward-compatible evolution means clients never need to update.
Rules for backward-compatible changes:
- Never remove fields - Clients might be reading them. Deprecate first, remove later.
- Never change field types -
stringtointegerbreaks clients. - Never change field semantics - If
status: "active"meant one thing, do not change what it means. - Add fields as optional - New fields should not be required. Old clients will ignore them.
- Be liberal in what you accept - Accept both old and new formats during transitions.
- Be conservative in what you send - Do not send fields clients do not expect.
The Robustness Principle (Postel’s Law)
“Be conservative in what you do, be liberal in what you accept from others.”
Applied to APIs: accept both the old field name and the new field name during a transition. Send the new field name. Old clients that send the old name still work. New clients use the new name.
Where it breaks or gets interesting
Version proliferation
If you never deprecate old versions, you end up maintaining v1, v2, v3, v4, v5 simultaneously. Each version has different behavior. Bugs must be fixed in all versions. New features must be backported. This is unsustainable.
Set a deprecation policy: versions are supported for 12-24 months after a new version is released. Communicate deprecation timelines clearly. Monitor which clients are still using old versions and reach out to them.
The sunset header
When deprecating an API version, return a Sunset header with the deprecation date:
Sunset: Sat, 31 Dec 2024 23:59:59 GMT
Deprecation: true
Link: <https://docs.example.com/migration>; rel="deprecation"
Clients that monitor response headers can detect deprecation and alert their teams.
Internal vs external versioning
Internal APIs (between your own microservices) and external APIs (for third-party clients) have different versioning needs. Internal APIs can be updated more aggressively because you control all clients. External APIs need longer deprecation windows because you do not control when clients update.
For internal APIs, consider using a schema registry (Protobuf, Avro) with compatibility checks rather than URL versioning.
GraphQL versioning
GraphQL does not use URL versioning. Instead, fields are deprecated with @deprecated(reason: "Use newField instead"). Clients can still use deprecated fields. The schema evolves without breaking clients. Old fields are removed only after all clients have migrated.
This is the “versionless API” approach: continuous evolution without explicit versions.
Real-world systems
Stripe - URL versioning (/v1/) plus date-based header versioning (Stripe-Version: 2024-01-01). The URL version is fixed at v1 (they have not needed a v2). The date version controls behavior changes within v1. New accounts get the latest date version by default. Existing accounts are locked to the date version when they first made an API call.
GitHub - REST API v3 (URL versioning). GraphQL API v4 (no versioning, schema evolution). Deprecated v3 endpoints return Deprecation headers.
Twilio - URL versioning (/2010-04-01/). The version is a date, embedded in the URL. Very stable - the 2010 version is still supported.
Salesforce - URL versioning with major version numbers (/v58.0/). New versions released twice per year. Old versions supported for 3 years.
AWS - Query parameter versioning for some services (Action=DescribeInstances&Version=2016-11-15). Header versioning for others. Extremely long support windows (some APIs from 2006 still work).
How to apply it in practice
The versioning lifecycle
- Design - Design the API to be backward compatible from the start
- Deprecate - Mark old fields/endpoints as deprecated with a timeline
- Migrate - Help clients migrate with documentation and migration guides
- Remove - Remove deprecated elements after the sunset date
Versioning checklist for API changes
Before making a change, ask:
- Is this a breaking change? (removes fields, changes types, changes semantics)
- Can I make it backward compatible? (add new field alongside old, accept both formats)
- If breaking: do I need a new version, or can I use feature flags?
- What is the migration path for existing clients?
- What is the deprecation timeline?
Feature flags as an alternative
For some changes, feature flags are better than versioning. A flag like ?use_new_pagination=true lets clients opt into new behavior before it becomes the default. Once all clients have opted in, make it the default and remove the flag.
FAQ
Q: How long should you support old API versions?
It depends on your client base. For public APIs with many third-party integrations: 12-24 months minimum. For internal APIs: 3-6 months. For mobile apps (where users do not always update): longer, because old app versions may still be in use. Monitor usage of old versions and extend support if significant traffic remains.
Q: Should you version every change or only breaking changes?
Only breaking changes require a new version. Non-breaking changes (adding optional fields, adding endpoints) can be deployed without a version bump. Versioning every change creates unnecessary complexity and forces clients to update for no reason.
Q: How do you handle versioning for event-driven APIs (webhooks)?
Include a version field in the webhook payload: {"version": "2024-01-15", "event": "order.shipped", ...}. Clients check the version and handle accordingly. When you change the payload format, bump the version. Clients that have not updated to handle the new version can still process old-format events. Provide a way for clients to specify which version of events they want to receive.
Interview questions
Q1: You need to rename a field in your API response from user_name to username. How do you do this without breaking existing clients?
Strong answer: Use the expand-contract pattern. Phase 1: add username to the response alongside user_name. Both fields are present. New clients use username. Old clients continue using user_name. Phase 2: deprecate user_name - add a deprecation notice in documentation, return a Deprecation header, log which clients are still using user_name. Phase 3: after the deprecation window (e.g., 6 months), remove user_name. Monitor for any clients that break and reach out to them. The key: never remove a field without first adding the replacement and giving clients time to migrate.
Q2: Your API has v1, v2, v3, and v4. Maintaining all four is expensive. How do you reduce this?
Strong answer: Establish a deprecation policy and enforce it. Announce that v1 and v2 will be sunset in 6 months. Send emails to all API key holders using v1 and v2. Return Sunset and Deprecation headers on all v1 and v2 responses. Provide a migration guide. Monitor usage - if a client is still on v1 after 3 months, reach out directly. After the sunset date, return 410 Gone for v1 and v2 requests. Going forward, maintain at most 2 versions simultaneously (current and previous). Set a policy: new versions are released when breaking changes are needed, old versions are deprecated 12 months after the new version is released.
Q3: How would you design the versioning strategy for a public API that will be used by thousands of third-party developers?
Strong answer: Use URL versioning for discoverability and simplicity (/v1/). Design the v1 API to be as stable as possible - think carefully about field names, types, and semantics before releasing. Use date-based sub-versioning (like Stripe) for behavior changes within v1: new accounts get the latest behavior, existing accounts are locked to the behavior at their first API call. This prevents surprise behavior changes for existing integrations. Commit to a 24-month support window for each major version. Provide a changelog, migration guides, and a deprecation dashboard. Use semantic versioning for your API: v1 is stable, v2 is a major breaking change. Avoid v2 as long as possible by designing v1 to be extensible.