API Versioning Strategies
How you version your API determines how painful it is to evolve it — and how painful it is for your clients to migrate. This guide covers every versioning approach with real request examples, a clear pros/cons matrix, and the Sunset/deprecation headers that make migrations manageable.
Strategy 1: URL Path Versioning
The version is part of the URL path. Used by Stripe, Twilio, GitHub, and most public APIs.
GET https://api.example.com/v1/users/42
POST https://api.example.com/v2/users
# Express routing — mount per-version routers
import express from 'express';
import v1Routes from './routes/v1';
import v2Routes from './routes/v2';
const app = express();
app.use('/v1', v1Routes);
app.use('/v2', v2Routes);
# Or use a single router with version middleware
app.use('/v:version', (req, res, next) => {
req.apiVersion = parseInt(req.params.version);
next();
}, mainRouter); # nginx — route by URL prefix to versioned backends
location /v1/ {
proxy_pass http://api-v1:3000/;
}
location /v2/ {
proxy_pass http://api-v2:3001/;
}
# Default (no version prefix) → latest stable
location /api/ {
proxy_pass http://api-v2:3001/;
} Strategy 2: Request Header Versioning
The URL stays clean; clients declare the version in a request header.
# Option A: custom header GET /users/42 X-API-Version: 2 # Option B: Accept header (content negotiation, RFC 6838) GET /users/42 Accept: application/vnd.acme.v2+json # Option C: Stripe-style date-based versions GET /users/42 Stripe-Version: 2024-06-20
// Express middleware — extract version from header
function apiVersionMiddleware(req, res, next) {
const headerVersion = req.headers['x-api-version'];
const acceptVersion = (req.headers['accept'] ?? '')
.match(/vnd\.acme\.v(\d+)\+json/)?.[1];
req.apiVersion = parseInt(headerVersion ?? acceptVersion ?? '1');
// Tell CDN to cache separately per version
res.setHeader('Vary', 'X-API-Version, Accept');
next();
}
app.use(apiVersionMiddleware);
app.get('/users/:id', (req, res) => {
if (req.apiVersion >= 2) {
return res.json(buildUserResponseV2(req.params.id));
}
return res.json(buildUserResponseV1(req.params.id));
}); Strategy 3: Query Parameter Versioning
Simple but mixes routing concerns with filtering. Avoid for public APIs.
GET /users/42?version=2 GET /users/42?v=2&format=json # Simple to implement but has problems: # - Easy to accidentally omit from generated URLs # - Some proxies strip query params from cache keys # - Harder to route at the infrastructure level # - Mixes version (routing concern) with filters (query concern)
Sunset & Deprecation Headers
// Express middleware — attach deprecation headers to v1 responses
function deprecationHeaders(req, res, next) {
// Deprecation: when this version was deprecated (past date)
res.setHeader('Deprecation', 'Mon, 01 Jul 2024 00:00:00 GMT');
// Sunset: when this version will be removed (future date)
res.setHeader('Sunset', 'Fri, 01 Jan 2026 00:00:00 GMT');
// Link to migration guide
res.setHeader(
'Link',
'<https://docs.example.com/v2/migration>; rel="successor-version", ' +
'<https://docs.example.com/changelog>; rel="deprecation"'
);
next();
}
// Apply only to v1 routes
app.use('/v1', deprecationHeaders, v1Router); // Client-side: detect Sunset header and warn developers
async function apiRequest(url: string, options?: RequestInit) {
const response = await fetch(url, options);
const sunset = response.headers.get('Sunset');
if (sunset) {
const sunsetDate = new Date(sunset);
const daysUntil = Math.floor(
(sunsetDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
);
console.warn(
`[API] Warning: This endpoint is deprecated and will be removed ` +
`in ${daysUntil} days (${sunset}). ` +
`See Link header for migration guide.`
);
}
return response;
} Pros & Cons Comparison
| Strategy | Pros | Cons | Best for |
|---|---|---|---|
| URL path (/v1/) | Visible, cacheable, easy routing, curl-friendly | Version in URL is "not RESTful" | Public APIs, SDK-heavy clients |
| Custom header | Clean URLs, server-side routing control | Not browser-testable, Vary cache complexity | Internal APIs, controlled clients |
| Accept header | RFC-compliant, content negotiation | Complex media types, poor DX | Hypermedia APIs |
| Query param (?v=2) | Simple to implement | Hard to route, cache issues, poor DX | Prototypes only |
| Date-based (Stripe) | Granular control, no major versions | Complex compatibility matrix | Mature APIs with long history |
Breaking vs Non-Breaking Changes
| Change Type | Breaking? | Action required |
|---|---|---|
| Add new optional response field | No | Safe — deploy anytime |
| Add new endpoint | No | Safe — deploy anytime |
| Remove a response field | Yes | New version required |
| Rename a field | Yes | New version required |
| Change field type (string→int) | Yes | New version required |
| Make optional field required | Yes | New version required |
| Change error response shape | Yes | New version required |
| Change auth mechanism | Yes | New version + migration guide |
| Add new enum value to response | Soft | Communicate ahead; clients should use default/else |
Migration Checklist
# API Version Migration Checklist
# --------------------------------
# 1. Announce deprecation with at least 6-month timeline
# - Blog post, email to API key holders, changelog entry
# - Start sending Deprecation + Sunset headers immediately
# 2. Publish migration guide
# - Diff of breaking changes
# - Before/after code examples for each change
# - Link in Sunset Link header
# 3. Track adoption via analytics
# - Log X-API-Version / URL prefix per API key
# - Identify clients still on old version at 3mo, 1mo, 1wk marks
# 4. Email stragglers
# SELECT api_key, last_seen FROM api_requests
# WHERE version = 'v1' AND last_seen > NOW() - INTERVAL '30 days';
# 5. Sunset the version
# - Return 410 Gone with migration URL
# - Keep route handler, just return error
# app.use('/v1', (req, res) => res.status(410).json({
# error: {
# code: 'VERSION_SUNSET',
# message: 'API v1 has been removed. Please migrate to v2.',
# migration_guide: 'https://docs.example.com/v2/migration',
# }
# }))); Frequently Asked Questions
What is the most popular API versioning strategy?▼
URL path versioning (/v1/, /v2/) is by far the most widely used approach, employed by Stripe, Twilio, GitHub v3, and most large public APIs. It wins on practical grounds: URLs are self-describing, developers can see the version immediately, curl and browser testing work without setting headers, proxy caches and CDNs work correctly, and you can route traffic by URL prefix in any load balancer or API gateway. Despite being technically 'impure' from a REST perspective (the version is not a resource property), its developer experience advantages outweigh the theoretical objections.
When should I use header-based versioning?▼
Header versioning (Accept: application/vnd.myapi.v2+json or a custom X-API-Version header) is appropriate when you want clean URLs that never change — the same URL /users always points to the 'current' users resource but clients opt into a specific version via header. This is popular in content negotiation-heavy APIs and internal APIs where all clients are controlled. The downsides: impossible to test in a browser without dev tools, CDN caching is trickier (you must vary on the header: Vary: Accept), and many developers forget to set the header and get unexpected behavior when you ship v2.
What are Sunset headers and how do I use them?▼
The Sunset HTTP response header (RFC 8594) indicates when a deprecated API version will be removed. Format: Sunset: Sat, 01 Mar 2025 00:00:00 GMT. Also add a Deprecation header (IETF draft): Deprecation: Tue, 01 Sep 2024 00:00:00 GMT. Pair these with a Link header pointing to migration docs: Link: <https://docs.example.com/v2/migration>; rel='successor-version'. Send these headers on every response from deprecated endpoints — well-behaved API clients will log warnings when they detect Sunset headers, alerting developers to migrate before the deadline.
How do I avoid breaking changes when evolving an API?▼
Follow additive-only changes for minor versions: adding new optional fields, new endpoints, or new enum values to responses is always safe. Breaking changes (removing fields, changing field types, making optional fields required, changing URL structure) require a new version. Use feature flags or API preview programs to give early adopters access to breaking changes before the official version bump. Maintain a public changelog and announce deprecations at least 6 months before removal. Consider consumer-driven contract testing (Pact) to detect when your changes would break specific known clients.
Can I version a GraphQL API?▼
The GraphQL community's official guidance is to avoid versioning: instead, evolve the schema by adding new fields, deprecating old ones with @deprecated(reason: '...') directive, and eventually removing deprecated fields after clients have migrated. This works because GraphQL clients only request fields they need — adding fields is never breaking. For larger breaking changes, use a field migration pattern: add newField alongside the old field, announce deprecation, run a query analyzing tool to see if clients still use the old field, then remove it. If a true version break is unavoidable, use a separate endpoint (/graphql/v2) or API schema stitching.
How long should I maintain old API versions?▼
Industry standard is 12–24 months after a deprecation announcement, depending on your client base and contract terms. Stripe famously never removes API versions — they maintain backward compatibility forever. Twilio and SendGrid provide 12-month sunsets. GitHub deprecated v3 REST in favor of GraphQL but still maintains it. For enterprise APIs, 24 months is a safe default. Track which clients are still using deprecated versions via API key analytics — if a version has zero traffic for 90 days, you can safely remove it. Always send Sunset headers and email clients whose API keys are calling deprecated versions.
Related Products
on GumroadDevPlaybook Interview Vault
210+ real technical interview questions with model answers, asked at Google, Meta, Amazon, Microsoft, Stripe, and Shopify. Covers Frontend (55), Backend (55), System Design (50), and DSA (50). Available in English and Traditional Chinese.
Buy on Gumroad →TypeScript Quick Reference Toolkit
5 practical reference files for TypeScript developers: cheatsheet, TS vs JS comparison, 20 utility types with real examples, tsconfig best practices template, and 15 common mistakes guide. Print it. Keep it on your desk.
Buy on Gumroad →Developer Interview Vault
210 technical interview questions with model answers, key points, and follow-up questions. Covers Frontend, Backend, System Design, and DSA — bilingual English + Traditional Chinese (ZH-TW). PWYW from $29.
Buy on Gumroad →You Might Also Need
OpenAPI Validator
Paste your OpenAPI 3.x spec (YAML or JSON) to validate structure, find missing required fields, detect duplicate paths, check for missing operationIds, and get errors and warnings with path references.
Open free tool →
API Response Mock Generator
Generate realistic mock API responses with custom status codes, headers, and JSON body. Quick templates for 200, 401, 404, 429, 500 and more. Runs in your browser.
Open free tool →
API Response Time Tester
Test API endpoint response times directly from your browser. Run multiple requests, see average/min/max times, status codes, and response sizes. GET, POST, PUT, DELETE support. Free.
Open free tool →
More Free Tools
Related Articles
Complete HTTP Status Codes Reference for REST API Devs
The complete HTTP status codes reference for REST API developers. Every 1xx, 2xx, 3xx, 4xx, and 5xx code explained with real-world API examples and best practices.
REST API Best Practices: Endpoint Naming, Versioning, and Error Handling
Design clean, consistent REST APIs with proper URL naming, HTTP methods, versioning, error formats, pagination, auth, and rate limiting.
Understanding REST API Design Best Practices
A practical guide to designing REST APIs that developers love — covering resource modeling, HTTP semantics, status codes, versioning, and error handling.
curl 深度使用手冊:API 測試從入門到進階
curl 是每位開發者必備的 CLI 工具。本文涵蓋 GET/POST/PUT/DELETE 請求、認證、header 設定、檔案上傳、除錯技巧,以及 REST API 測試的完整實戰範例。
FastAPI vs Django REST Framework 2025: Which Should You Choose?
FastAPI vs Django REST Framework compared for 2025. Performance benchmarks, feature comparison, real-world use cases, and a clear decision guide for your next Python API project.
Enjoying this tool? Get more like it.
Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.
Unlock AI-Powered Dev Tools
- ⚡ AI Code Review, Doc Generator & SQL Builder
- ⚡ All premium templates & early access
- ⚡ Member discounts on Gumroad products
$9/mo after trial · Cancel anytime
Want the full toolkit?
Get DevPlaybook Pro
Every template, guide, boilerplate, and automation script in one bundle. 13 premium products — grab them all at once and save big.
MIT licensed · Instant download · No subscription
See what's included → Browse all deals →