API Versioning Strategies: URL Path, Headers, and GraphQL Evolution
API Versioning: URL Path, Headers, and GraphQL Evolution#
Your API will change. New fields, removed endpoints, restructured responses. The question isn't whether to version — it's how.
Why Version?#
Breaking changes without versioning:
Client expects: { "name": "Alice", "email": "alice@example.com" }
Server returns: { "full_name": "Alice Smith", "contact": { "email": "alice@example.com" } }
→ Client breaks
Versioning Strategies#
1. URL Path Versioning (Most Common)#
GET /api/v1/users/123
GET /api/v2/users/123
Pros: Explicit, easy to understand, easy to route, cacheable Cons: URL pollution, hard to sunset old versions Used by: Stripe, Twilio, GitHub
2. Header Versioning#
GET /api/users/123
Accept: application/vnd.myapi.v2+json
Or custom header:
GET /api/users/123
X-API-Version: 2
Pros: Clean URLs, version is metadata not resource identifier Cons: Less discoverable, harder to test in browser Used by: GitHub (accepts both URL and header)
3. Query Parameter#
GET /api/users/123?version=2
Pros: Simple, works in browser Cons: Optional parameter means version can be forgotten, cache key pollution Rarely recommended — use URL path or header instead.
4. Date-Based (Stripe's Approach)#
Stripe-Version: 2026-03-28
Each API version is a date. New accounts get the latest. Old accounts keep their version until they explicitly upgrade.
Pros: Fine-grained (per-change, not batched), backwards compatible by default Cons: Complex to implement (need version-specific response transformations) Best for: APIs with many small breaking changes over time
5. GraphQL (No Versioning)#
GraphQL doesn't version — it evolves:
type User {
name: String! # always available
email: String! # always available
fullName: String # added later — old clients don't request it
bio: String @deprecated(reason: "Use profile.bio instead")
}
Pros: No versions to maintain, backwards compatible by design Cons: Can't remove fields (only deprecate), schema grows over time Best for: APIs with many clients requesting different fields
Comparison#
| Strategy | Simplicity | Discoverability | Caching | Breaking Changes |
|---|---|---|---|---|
| URL path | High | High | Easy | New URL per version |
| Header | Medium | Low | Harder | Same URL, different response |
| Query param | High | Medium | Cache key bloat | Optional = risky |
| Date-based | Low | Low | Complex | Per-change granularity |
| GraphQL | High | High (introspection) | Custom | Additive only |
Migration Strategies#
Sunset Header#
Tell clients when a version will die:
Sunset: Sat, 01 Jan 2027 00:00:00 GMT
Deprecation: true
Link: <https://api.example.com/v3/docs>; rel="successor-version"
Gradual Migration#
- Announce deprecation (6 months notice minimum)
- Add sunset headers to v1 responses
- Monitor v1 traffic — contact heavy users directly
- Return warnings in v1 responses
- Turn off v1 after sunset date
Versioned Transformers#
Keep one internal model, transform per version:
// Internal model (always latest)
const user = await getUser(id);
// Transform per requested version
if (version === "v1") {
return { name: user.name, email: user.email }; // v1 shape
}
if (version === "v2") {
return { full_name: user.fullName, contact: { email: user.email } }; // v2 shape
}
Best Practices#
- Default to URL path versioning — simplest, most understood
- Version from day one —
/api/v1/even if you think you won't need v2 - Never remove fields without versioning — additive changes don't need new versions
- Deprecate before removing — give clients time to migrate
- Document migration guides — for each version bump
- Monitor version usage — know who's on what version before sunsetting
- Consider GraphQL if you have many clients with different data needs
What Counts as a Breaking Change?#
| Change | Breaking? | Action |
|---|---|---|
| Add optional field to response | No | No version bump |
| Add required field to request | Yes | New version |
| Remove field from response | Yes | New version |
| Rename field | Yes | New version |
| Change field type (string → number) | Yes | New version |
| Change error format | Yes | New version |
| Add new endpoint | No | No version bump |
| Change authentication method | Yes | New version |
Design your API architecture at codelit.io — generate interactive diagrams with 29 export formats.
111 articles on system design at codelit.io/blog.
Try it on Codelit
GitHub Integration
Paste any repo URL to generate an interactive architecture diagram from real code
Related articles
Try these templates
OpenAI API Request Pipeline
7-stage pipeline from API call to token generation, handling millions of requests per minute.
8 componentsDistributed Rate Limiter
API rate limiting with sliding window, token bucket, and per-user quotas.
7 componentsAPI Gateway Platform
Kong/AWS API Gateway-like platform with routing, auth, rate limiting, transformation, and developer portal.
8 componentsBuild this architecture
Generate an interactive architecture for API Versioning Strategies in seconds.
Try it in Codelit →
Comments