Skip to main content

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.

✓ URL, header & content-type strategies ✓ Sunset & deprecation headers ✓ Migration patterns ✓ Pros/cons comparison table

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-friendlyVersion in URL is "not RESTful"Public APIs, SDK-heavy clients
Custom headerClean URLs, server-side routing controlNot browser-testable, Vary cache complexityInternal APIs, controlled clients
Accept headerRFC-compliant, content negotiationComplex media types, poor DXHypermedia APIs
Query param (?v=2)Simple to implementHard to route, cache issues, poor DXPrototypes only
Date-based (Stripe)Granular control, no major versionsComplex compatibility matrixMature APIs with long history

Breaking vs Non-Breaking Changes

Change Type Breaking? Action required
Add new optional response fieldNoSafe — deploy anytime
Add new endpointNoSafe — deploy anytime
Remove a response fieldYesNew version required
Rename a fieldYesNew version required
Change field type (string→int)YesNew version required
Make optional field requiredYesNew version required
Change error response shapeYesNew version required
Change auth mechanismYesNew version + migration guide
Add new enum value to responseSoftCommunicate 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: &lt;https://docs.example.com/v2/migration&gt;; 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.

Share this tool: X / Twitter LinkedIn
✓ 60,000+ developers use DevPlaybook ★★★★★ 4.8 / 5 on Gumroad ✓ MIT licensed · Instant download ✓ No login required

Related Products

on Gumroad

You Might Also Need

More Free Tools

Enjoying this tool? Get more like it.

Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.

DevPlaybook Pro 7 days free

Unlock AI-Powered Dev Tools

  • ⚡ AI Code Review, Doc Generator & SQL Builder
  • ⚡ All premium templates & early access
  • ⚡ Member discounts on Gumroad products
Start Free Trial →

$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.

$79
$241 separately Save $162 (67% off)
Get DevPlaybook Pro — $79 →

MIT licensed · Instant download · No subscription

See what's included → Browse all deals →