API Versioning Cheat Sheet

Quick reference for API versioning: URI versioning, header versioning, content negotiation, deprecation strategies, and migration patterns.

Strategies Changes Deprecation Headers Examples Migration

API Versioning Strategies

Strategy Example Pros Cons Used By
URI Path /v1/users Simple, visible, cacheable Pollutes URLs, REST purists dislike Stripe, GitHub, Twitter
Query Param /users?version=1 Simple, flexible Less visible, caching issues Some internal APIs
Custom Header X-API-Version: 1 Clean URLs, flexible Less visible, requires docs Many enterprise APIs
Accept Header Accept: application/vnd.api.v1+json RESTful, content negotiation Complex, less intuitive GitHub (alternative)
Media Type Content-Type: application/vnd.company.v1+json Standard content negotiation Verbose, requires client config Some REST APIs
Date-Based Accept-Version: 2024-01-01 Clear timeline, auto-expiry Requires date tracking Stripe (for features)

Breaking vs Non-Breaking Changes

⚠ Breaking Changes (require new version)

  • × Removing an endpoint
  • × Removing a response field
  • × Changing a field's type
  • × Changing authentication method
  • × Modifying error response format
  • × Changing field behavior/semantics
  • × Removing required parameters
  • × Changing status codes

✓ Non-Breaking Changes (backward compatible)

  • Adding new endpoints
  • Adding optional response fields
  • Adding optional request parameters
  • Adding new enum values
  • Extending field length limits
  • Adding new HTTP methods to endpoints
  • Performance improvements
  • Bug fixes (that don't change behavior)

Deprecation Strategy

Phase Duration Actions Communication
Announce Day 0 Publish deprecation notice, migration guide Blog, email, dashboard banner, docs
Warn 6 months before EOL Add Deprecation header to responses Response headers, API logs, emails
Sunset EOL date Return 410 Gone or redirect Final email, last-chance notice
Retire After EOL Remove old version code Post-mortem blog (optional)
# Deprecation Response Headers
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"
X-API-Warn: This endpoint is deprecated. Please migrate to /v2/users by Dec 31, 2026.

# RFC 8631 Deprecation format
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: </docs/migration-v2>; rel="deprecation"; type="text/html"

Version-Related HTTP Headers

Header Direction Example
X-API-Version Request 2
Accept-Version Request 2024-01-01
Accept Request application/vnd.api.v2+json
API-Version Response 2.1.0
Deprecation Response true
Sunset Response Sat, 31 Dec 2026 23:59:59 GMT
X-API-Warn Response This endpoint is deprecated...

Real-World API Versioning Examples

Stripe — URI Versioning

# URI versioning (default)
GET https://api.stripe.com/v1/charges

# Date-based versioning for features
curl https://api.stripe.com/v1/charges \
  -H "Stripe-Version: 2024-01-01"

# Account-level default version
# Set in Dashboard → Settings → API Version

# Response includes version
{
  "id": "ch_123",
  "object": "charge",
  "livemode": false
}

GitHub — URI + Accept Header

# URI versioning (primary)
GET https://api.github.com/v1/users/octocat

# Accept header for media type versions
GET https://api.github.com/users/octocat \
  -H "Accept: application/vnd.github.v3+json"

# Beta features via accept header
GET https://api.github.com/installation/repositories \
  -H "Accept: application/vnd.github.machine-man-preview+json"

# Versioned error responses
{
  "message": "Validation Failed",
  "errors": [...],
  "documentation_url": "https://docs.github.com/rest"
}

Twilio — URI Versioning with Year

# Year-based URI versioning
GET https://api.twilio.com/2010-04-01/Accounts/{sid}/Calls.json
GET https://api.twilio.com/2015-04-01/Accounts/{sid}/Messages.json

# Version in response
{
  "accounts": [...],
  "next_page_uri": "/2010-04-01/Accounts/...",
  "uri": "/2010-04-01/Accounts.json"
}

Migration Patterns

Pattern 1: Side-by-Side Deployment

# Both versions run simultaneously
/v1/users  → routes to UserControllerV1
/v2/users  → routes to UserControllerV2

# Router config (Express.js example)
app.use('/v1/users', require('./controllers/v1/users'));
app.use('/v2/users', require('./controllers/v2/users'));

# Shared models, different transformers
const User = require('./models/User');

// V1 controller
exports.list = async (req, res) => {
  const users = await User.findAll();
  res.json(users.map(u => transformV1(u)));  // Old format
};

// V2 controller
exports.list = async (req, res) => {
  const users = await User.findAll();
  res.json(users.map(u => transformV2(u)));  // New format
};

Pattern 2: Adapter/Facade

# Single implementation, version-specific adapters
class UserService {
  async getUser(id) { /* core logic */ }
}

class UserAdapterV1 {
  format(user) {
    return { id: user.id, name: user.full_name };  // Snake case
  }
}

class UserAdapterV2 {
  format(user) {
    return { id: user.id, name: user.fullName };  // Camel case
  }
}

# Router selects adapter based on version
app.get('/users/:id', (req, res) => {
  const version = req.headers['x-api-version'] || 'v1';
  const adapter = version === 'v2' ? new UserAdapterV2() : new UserAdapterV1();
  const user = await userService.getUser(req.params.id);
  res.json(adapter.format(user));
});

Pattern 3: Gradual Rollout

# Feature flag controlled migration
const MIGRATION_PERCENTAGE = 10;  // 10% of traffic to v2

app.get('/users/:id', (req, res) => {
  // Check if user is in migration cohort
  const userHash = hash(req.params.id) % 100;

  if (userHash < MIGRATION_PERCENTAGE) {
    // Route to v2 (new implementation)
    return v2Controller.get(req, res);
  }

  // Default to v1
  v1Controller.get(req, res);
});

# Gradually increase: 10% → 25% → 50% → 75% → 100%

Best Practices

Use URI versioning for simplicity and visibility

Maintain at least 6-12 months overlap between versions

Communicate deprecation early and often

Provide migration guides with code examples

Use Deprecation and Sunset headers for machine-readable warnings

Version SDKs alongside API versions

Don't break existing clients without long notice

Don't version everything—only when behavior changes

Related Resources