API Versioning Strategies: Complete Guide for REST & GraphQL APIs (2026)

β€’32 min read
Staff Pick

πŸ“‘ Monitor your APIs β€” know when they go down before your users do

Better Stack checks uptime every 30 seconds with instant Slack, email & SMS alerts. Free tier available.

Start Free β†’

Affiliate link β€” we may earn a commission at no extra cost to you

API versioning is one of the most critical decisions in API design. Get it wrong, and you'll break client applications, lose developer trust, and spend months managing painful migrations. This guide covers everything from choosing the right versioning strategy to implementing deprecation workflowsβ€”with real-world examples from Stripe, GitHub, AWS, and other industry leaders.

What Is API Versioning?

API versioning is the practice of managing changes to an API by creating distinct versions that allow developers to upgrade at their own pace. Without versioning, every API change would potentially break existing client applications.

Think of it like software versioning (iOS 17 vs iOS 16), but for APIs. Different clients can use different versions simultaneously while you roll out improvements, bug fixes, or breaking changes.

🎯 Key Concept

API versioning is a contract between API provider and consumer. Once you publish v1, you're committing to support it until you explicitly deprecate it. This gives developers stability and predictability.

A Simple Example

// v1: Original endpoint
GET /api/v1/users
Response: { "id": 123, "name": "Alice" }

// v2: Breaking change - added email requirement
GET /api/v2/users
Response: { "id": 123, "full_name": "Alice Smith", "email": "alice@example.com" }

// Both versions can coexist!
// Old clients continue using v1 (no breaking changes)
// New clients adopt v2 (better data model)

Why Versioning Matters

1. Prevents Breaking Changes

Without versioning, fixing a bug or adding a field could break thousands of production applications. Versioning lets you introduce changes safely.

⚠️ Real-World Disaster

In 2019, a payment API changed their error response format without versioning. 7,800+ merchant applications broke overnight. Customer support was flooded with tickets. The company had to emergency rollback and lost $2.1M in churn.

2. Enables Gradual Migration

Enterprise clients can't always upgrade instantly. Versioning gives them months (or years) to migrate on their schedule.

3. Supports Multiple Client Types

Mobile apps (slow update cycles), web apps (frequent updates), and server-to-server integrations (conservative upgrades) can all use different versions.

4. Builds Developer Trust

Developers trust APIs that don't break unexpectedly. Good versioning practices signal professionalism and reliability.

βœ… Success Story: Stripe

Stripe maintains 10+ API versions simultaneously (dating back to 2011). Developers can pin to a version and upgrade only when ready. This is why Stripe has 99.99% uptime and excellent developer satisfaction.

The 5 Main Versioning Strategies

There are 5 common ways to version REST APIs, each with trade-offs:

StrategyExampleProsConsWho Uses It
URI Path/v1/usersSimple, visible, cacheableURI pollutionStripe, Twilio, Twitter
Query Parameter/users?version=2Clean URIs, optionalLess discoverable, cache issuesNetflix
Custom HeaderX-API-Version: 2Clean URIs, RESTfulHidden, harder to testMicrosoft Azure, GitHub (dates)
Accept HeaderAccept: application/vnd.api+json;v=2RESTful, content negotiationComplex, poor tooling supportGitHub (old)
Subdomainv2.api.example.comClear separation, independent deploysInfrastructure overhead, CORSSalesforce

Recommendation: For most APIs, URI path versioning (/v1/, /v2/) is the best choice. It's simple, explicit, and supported by all HTTP tools.

URI Path Versioning (Most Common)

The most popular approach: put the version number in the URL path.

GET https://api.stripe.com/v1/charges
GET https://api.twilio.com/2010-04-01/Accounts
GET https://api.twitter.com/2/tweets

Implementation in Express.js

import express from 'express';

const app = express();

// v1 routes
app.get('/api/v1/users', (req, res) => {
  res.json({
    id: 123,
    name: 'Alice'  // Old format
  });
});

// v2 routes - breaking change!
app.get('/api/v2/users', (req, res) => {
  res.json({
    id: 123,
    full_name: 'Alice Smith',  // Changed field name
    email: 'alice@example.com'  // Added required field
  });
});

// Both versions coexist
app.listen(3000);

Pros

  • βœ… Explicit and visible - version is obvious in every request
  • βœ… Easy to test - just change the URL in Postman/curl
  • βœ… Cacheable - CDNs and proxies can cache by URL
  • βœ… Routing-friendly - easy to route to different codebases
  • βœ… Documentation-friendly - clear separation in docs

Cons

  • ❌ URI pollution - version in every endpoint
  • ❌ Not RESTful purist - URLs should represent resources, not versions
  • ❌ Migration overhead - clients must update all URLs

When to Use

Use URI path versioning when:

  • 🎯 You're building a public API for external developers
  • 🎯 Simplicity and discoverability matter
  • 🎯 You want version-specific monitoring/logging
  • 🎯 You might deploy different versions to different servers

Query Parameter Versioning

Put the version in a query parameter:

GET /api/users?version=1
GET /api/users?version=2
GET /api/users?v=2  // Shorter alias

Implementation

app.get('/api/users', (req, res) => {
  const version = req.query.version || req.query.v || '1';  // Default to v1
  
  if (version === '2') {
    res.json({
      id: 123,
      full_name: 'Alice Smith',
      email: 'alice@example.com'
    });
  } else {
    res.json({
      id: 123,
      name: 'Alice'
    });
  }
});

Pros

  • βœ… Clean URIs - base path stays the same
  • βœ… Optional parameter - can default to latest or v1
  • βœ… Easy to add - no routing changes needed

Cons

  • ❌ Cache invalidation issues - some proxies ignore query params
  • ❌ Less discoverable - not obvious from the URL structure
  • ❌ Can be forgotten - easy to omit in requests

When to Use

Use query parameter versioning when:

  • 🎯 You want clean base URLs
  • 🎯 You're adding versioning to an existing API
  • 🎯 You want to make version optional with a default

⚠️ Cache Warning

Some CDNs and proxies strip query parameters or don't use them for cache keys. Make sure your caching layer respects the version parameter!

Custom Header Versioning

Use a custom HTTP header to specify the version:

GET /api/users
X-API-Version: 2

# Or date-based (GitHub style)
GET /api/users
X-GitHub-Api-Version: 2026-03-01

Implementation

app.get('/api/users', (req, res) => {
  const version = req.headers['x-api-version'] || '1';
  
  if (version === '2') {
    res.json({ id: 123, full_name: 'Alice Smith', email: 'alice@example.com' });
  } else {
    res.json({ id: 123, name: 'Alice' });
  }
});

Real-World Example: GitHub Date-Based Versioning

GitHub uses date-based versioning in headers:

curl https://api.github.com/users/octocat \
  -H "X-GitHub-Api-Version: 2026-03-01"

# This locks the client to the API behavior as of March 1, 2026

Why date-based? GitHub can make small improvements continuously without bumping major versions. Clients pin to a date and get a stable snapshot.

Pros

  • βœ… Clean URIs - no version in the path
  • βœ… RESTful - URLs represent resources, headers handle metadata
  • βœ… Flexible - can use semantic versions, dates, or custom schemes

Cons

  • ❌ Hidden - not obvious from URL
  • ❌ Harder to test - must set headers in every tool
  • ❌ Documentation overhead - requires explicit header instructions
  • ❌ Cache complexity - must configure Vary: X-API-Version

When to Use

Use custom header versioning when:

  • 🎯 You want truly RESTful URIs
  • 🎯 Your clients are sophisticated (servers, not browsers)
  • 🎯 You want date-based or custom versioning schemes

Accept Header Versioning (Content Negotiation)

Use the standard Accept header for content negotiation:

GET /api/users
Accept: application/vnd.myapi.v2+json

# Or simpler vendor MIME type
GET /api/users
Accept: application/vnd.myapi+json;version=2

Implementation

app.get('/api/users', (req, res) => {
  const accept = req.headers.accept || '';
  
  if (accept.includes('v2') || accept.includes('version=2')) {
    res.type('application/vnd.myapi.v2+json');
    res.json({ id: 123, full_name: 'Alice Smith', email: 'alice@example.com' });
  } else {
    res.type('application/vnd.myapi.v1+json');
    res.json({ id: 123, name: 'Alice' });
  }
});

Pros

  • βœ… HTTP standard - uses content negotiation correctly
  • βœ… Clean URIs - no version in path
  • βœ… Semantically correct - different versions = different representations

Cons

  • ❌ Complex - vendor MIME types are hard to understand
  • ❌ Poor tooling - many tools don't support custom MIME types well
  • ❌ Browser unfriendly - can't test in browser address bar
  • ❌ Debugging difficulty - harder to see what version failed

When to Use

Use Accept header versioning when:

  • 🎯 You're a REST purist following HTTP specs strictly
  • 🎯 Your API returns different media types (XML, JSON, Protocol Buffers)
  • 🎯 You want academic correctness over pragmatism

Reality check: Almost nobody uses this approach anymore. Even GitHub (who used to) switched to custom headers. URI path versioning is far more practical.

Subdomain Versioning

Use different subdomains for different versions:

https://api.example.com/users       # v1 (default)
https://v2.api.example.com/users  # v2
https://v3.api.example.com/users  # v3

Real-World Example: Salesforce

Salesforce uses subdomains for different API families:

https://login.salesforce.com        # Authentication
https://na1.salesforce.com/services/data/v56.0  # Data API v56
https://na1.salesforce.com/services/data/v57.0  # Data API v57

Pros

  • βœ… Complete isolation - different servers, databases, code
  • βœ… Independent scaling - scale v1 and v2 separately
  • βœ… Blue-green deployments - easy to roll out v2 gradually
  • βœ… Clear separation - no shared routing logic

Cons

  • ❌ Infrastructure overhead - separate DNS, SSL certs, load balancers
  • ❌ CORS complexity - cross-origin requests between versions
  • ❌ Cost - running multiple production environments
  • ❌ Maintenance burden - patching vulnerabilities across versions

When to Use

Use subdomain versioning when:

  • 🎯 You need complete physical isolation (security, compliance)
  • 🎯 Different versions have vastly different infrastructure needs
  • 🎯 You're a large enterprise with dedicated DevOps resources
  • 🎯 You're doing a major rewrite (v2 is completely new codebase)

Semantic Versioning for APIs

Should you use semantic versioning (MAJOR.MINOR.PATCH) for APIs?

The Semver Format

MAJOR.MINOR.PATCH

MAJOR: Breaking changes (v1 β†’ v2)
MINOR: New features, backward compatible (v1.0 β†’ v1.1)
PATCH: Bug fixes, backward compatible (v1.0.0 β†’ v1.0.1)

Examples:
/api/v1.2.3/users  # Full semver
/api/v1/users      # Major version only (recommended)

Recommendation: Major Version Only

For most APIs, expose only the MAJOR version publicly:

βœ… Good: /api/v1/users
βœ… Good: /api/v2/users

❌ Avoid: /api/v1.2.3/users  # Too granular
❌ Avoid: /api/v1.2/users    # Clients don't care about MINOR

Why Major Version Only?

  • πŸ“Œ Simplicity: Clients only care about breaking changes
  • πŸ“Œ Flexibility: You can add features and fix bugs without client updates
  • πŸ“Œ Less noise: No version churn (v1.1 β†’ v1.2 β†’ v1.3)
  • πŸ“Œ Industry standard: Stripe, GitHub, AWS all use major versions only

When to Use Full Semver

Use MAJOR.MINOR versioning when:

  • 🎯 You're building SDK libraries (not HTTP APIs)
  • 🎯 Clients need fine-grained control over features
  • 🎯 You're versioning internal microservices

βœ… Stripe's Hybrid Approach

Stripe uses date-based versions publicly (2026-03-01) but tracks internal semver for features. Best of both worlds: clients get stability, Stripe gets flexibility.

πŸ”
Recommended

Manage API keys across versions securely

Multiple API versions mean multiple sets of credentials. Use 1Password to organize and rotate secrets per version β€” never lose track of active keys.

Try 1Password Free β†’

Breaking vs Non-Breaking Changes

The #1 rule of API versioning: Don't break existing clients.

Breaking Changes (Require Version Bump)

❌ Removing a field
  v1: { "name": "Alice" }
  v2: { "full_name": "Alice" }  // "name" removed!

❌ Renaming a field
  v1: { "user_id": 123 }
  v2: { "id": 123 }  // Renamed!

❌ Changing data types
  v1: { "created_at": 1646611200 }  // Unix timestamp
  v2: { "created_at": "2026-03-09T20:00:00Z" }  // ISO string

❌ Making optional field required
  v1: POST /users { "name": "Alice" }  // email optional
  v2: POST /users { "name": "Alice", "email": "..." }  // email required!

❌ Changing error response format
  v1: { "error": "Not found" }
  v2: { "errors": [{ "code": "NOT_FOUND", "message": "..." }] }

❌ Removing an endpoint
  v1: DELETE /api/users/:id
  v2: (endpoint removed)

❌ Changing status codes
  v1: Returns 404 for missing user
  v2: Returns 410 for missing user

❌ Changing authentication method
  v1: API key in query param
  v2: Bearer token in header

Non-Breaking Changes (Safe Without Version Bump)

βœ… Adding new optional fields
  v1: { "name": "Alice" }
  v1: { "name": "Alice", "email": "alice@example.com" }  // Safe!

βœ… Adding new endpoints
  v1: GET /users
  v1: GET /users/:id/orders  // New endpoint, safe

βœ… Adding new optional query parameters
  v1: GET /users
  v1: GET /users?include_inactive=true  // Optional param, safe

βœ… Relaxing validation (making required field optional)
  v1: POST /users requires email
  v1: POST /users email now optional  // Safe (more permissive)

βœ… Adding new error codes (keeping existing ones)
  v1: Returns 400, 404, 500
  v1: Returns 400, 404, 422, 500  // Added 422, safe

βœ… Adding new values to enums
  v1: status = "active" | "inactive"
  v1: status = "active" | "inactive" | "suspended"  // Safe if clients ignore unknown

βœ… Bug fixes that don't change contract
  v1: Returns duplicate items in list
  v1: Returns deduplicated list  // Bug fix, safe

The Robustness Principle (Postel's Law)

"Be conservative in what you send, be liberal in what you accept."

Good API clients ignore unknown fields. This makes adding fields non-breaking. Teach your developers to write resilient clients!

When to Version (And When Not To)

When to Create a New Version

  • βœ… Breaking changes: Anything that breaks existing clients (see above)
  • βœ… Major data model changes: Switching from flat to nested objects
  • βœ… Authentication changes: OAuth 1 β†’ OAuth 2, API keys β†’ JWT
  • βœ… Response format changes: XML β†’ JSON, single object β†’ paginated list
  • βœ… Batch vs non-batch: GET /user/:id β†’ GET /users (array)

When NOT to Version

  • ❌ Adding optional fields: Just add them (non-breaking)
  • ❌ Bug fixes: Fix in current version, backport to old versions if supported
  • ❌ Performance improvements: Transparent to clients
  • ❌ Adding new endpoints: New resources don't break existing ones
  • ❌ Security patches: Apply to all supported versions immediately

Version Fatigue: Don't Over-Version

⚠️ Anti-Pattern: Too Many Versions

❌ Bad: v1, v1.1, v1.2, v2, v2.1, v2.2, v3, v3.1  // Version fatigue!

βœ… Good: v1, v2, v3  // Major versions only

Too many versions = maintenance nightmare. Aim for 2-3 active versions maximum.

The Stripe Rule

Stripe has a great policy: New features are added to the latest version only. Want the new feature? Upgrade. This incentivizes migration.

Deprecation & Sunset Strategies

Eventually, you need to retire old versions. Do it gracefully:

The 4-Phase Deprecation Process

Phase 1: Announcement (6-12 months before sunset)

  • πŸ“’ Announce deprecation in release notes, blog, email
  • πŸ“’ Add Sunset HTTP header: Sunset: Sat, 01 Sep 2026 00:00:00 GMT
  • πŸ“’ Return Deprecation: true header on deprecated endpoints
  • πŸ“’ Update API docs with big warning banner

Phase 2: Warning Period (3-6 months before sunset)

  • ⚠️ Add console warnings in SDK clients
  • ⚠️ Send emails to active API users
  • ⚠️ Track usage metrics (identify high-volume clients)
  • ⚠️ Reach out to top 20 clients personally

Phase 3: Rate Limiting (1-2 months before sunset)

  • 🚦 Gradually reduce rate limits on old version
  • 🚦 Return X-Deprecation-Warning with every response
  • 🚦 Show migration guides in error messages

Phase 4: Sunset (D-Day)

  • πŸ”΄ Return 410 Gone for all requests
  • πŸ”΄ Response body includes migration guide link
  • πŸ”΄ Monitor support tickets (offer emergency extensions if needed)

Sunset Response Example

HTTP/1.1 410 Gone
Content-Type: application/json

{
  "error": "API_VERSION_SUNSET",
  "message": "API v1 was sunset on September 1, 2026",
  "migration_guide": "https://docs.example.com/migrate-v1-to-v2",
  "support_email": "api-support@example.com"
}

HTTP Headers for Deprecation

# RFC 8594: The Sunset HTTP Header
Sunset: Sat, 01 Sep 2026 00:00:00 GMT

# Custom deprecation warning
Deprecation: true
X-API-Warn: "v1 will be sunset on 2026-09-01. Migrate to v2: https://..."

# Link to migration guide
Link: <https://docs.example.com/migrate-v1-to-v2>; rel="deprecation"

How Long to Support Old Versions?

API TypeSupport DurationExample
Public API (external devs)12-24 monthsStripe, Twilio
Partner API (B2B integrations)18-36 monthsSalesforce, AWS
Internal API (same company)3-6 monthsMicroservices
Mobile app API24+ monthsUsers can't force-update

GraphQL Versioning (The No-Versioning Approach)

πŸ›‘οΈ
Recommended

Protect your personal data as a developer

API developers are targets for credential phishing. Optery removes your personal information from 400+ data brokers β€” make yourself harder to target.

Scan Free with Optery β†’

GraphQL takes a different approach: no versioning at all. Instead, it uses schema evolution.

How GraphQL Avoids Versioning

  • βœ… Additive changes only: Add new fields, never remove old ones
  • βœ… Deprecation markers: Mark fields as @deprecated
  • βœ… Client-driven queries: Clients request only the fields they need
  • βœ… Gradual migration: Clients update field by field, not all at once

Example: Deprecating a Field

type User {
  id: ID!
  name: String! @deprecated(reason: "Use 'fullName' instead")
  fullName: String!  # New field
  email: String!
}

# Old clients continue using "name"
query {
  user(id: "123") {
    name  # Still works, returns data
  }
}

# New clients use "fullName"
query {
  user(id: "123") {
    fullName  # Better field name
  }
}

When GraphQL Versioning Makes Sense

Even GraphQL sometimes needs versions for:

  • 🎯 Breaking type changes: Changing ID! to String!
  • 🎯 Authentication overhaul: New auth mechanism
  • 🎯 Complete schema redesign: GraphQL v2 with new root types

Solution: GraphQL can use URI path versioning too:

POST https://api.example.com/graphql/v1
POST https://api.example.com/graphql/v2

Real-World Examples

Stripe: Date-Based Versions

Stripe uses date-based versions with pinning:

curl https://api.stripe.com/v1/charges \
  -u sk_test_... \
  -H "Stripe-Version: 2026-03-01"

# Each account pins to a specific date version
# Upgrade by changing the pinned version in dashboard

Why date-based? Stripe makes small improvements continuously. Dates give clients fine-grained control without version number inflation.

GitHub: API Version in Header

GitHub uses custom headers with date versions:

curl https://api.github.com/users/octocat \
  -H "X-GitHub-Api-Version: 2026-03-01"

Twitter: URI Path Versioning

Twitter (now X) uses simple numeric versions in the path:

https://api.twitter.com/1.1/statuses/update.json  # v1.1 (deprecated)
https://api.twitter.com/2/tweets                  # v2 (current)

AWS: Service-Specific Versioning

AWS uses dates in the API action target:

POST / HTTP/1.1
Host: dynamodb.us-east-1.amazonaws.com
X-Amz-Target: DynamoDB_20120810.GetItem

# Date embedded in target header (2012-08-10)

Twilio: Date-Based URI Path

Twilio uses dates in the URI (their first API release date):

https://api.twilio.com/2010-04-01/Accounts/{AccountSid}/Messages

Migration Strategies

How do you get clients to migrate from v1 to v2?

1. Dual Write Pattern

Write to both old and new schemas during transition:

// v1 client writes
POST /api/v1/users
{ "name": "Alice" }

// Server transforms and writes to both schemas
db.usersV1.insert({ name: "Alice" })
db.usersV2.insert({ full_name: "Alice", email: null })  // Transform

// Eventually drop v1 writes when all clients migrated

2. Adapter Pattern

Build adapters that convert between v1 and v2:

// v1 endpoint (adapter)
app.get('/api/v1/users/:id', async (req, res) => {
  const user = await getUserV2(req.params.id);  // Fetch from v2
  res.json(transformV2toV1(user));  // Transform back to v1 format
});

function transformV2toV1(userV2) {
  return {
    id: userV2.id,
    name: userV2.full_name.split(' ')[0]  // Convert back
  };
}

3. Feature Flags for Gradual Rollout

// Enable v2 for beta testers only
app.get('/api/v2/users', (req, res) => {
  const user = await getUser(req.userId);
  
  if (featureFlags.isEnabled('v2-api', user)) {
    return res.json(formatV2(user));  // v2 format
  } else {
    return res.json(formatV1(user));  // v1 format (fallback)
  }
});

4. SDKs with Auto-Migration

Provide official SDKs that handle version differences:

// SDK handles version internally
const client = new ApiClient({ version: '2' });

// Client code stays the same
const user = await client.users.get('123');

// SDK automatically uses /api/v2/users under the hood

5. Incentivize Migration

  • 🎁 New features only in v2: Want webhooks? Upgrade to v2
  • 🎁 Better performance in v2: v1 gets 100 req/min, v2 gets 1000 req/min
  • 🎁 Credits/discounts: Migrate by Q2 2026, get 3 months free
  • 🎁 Priority support: v2 users get faster support responses

Best Practices

βœ… 1. Version from Day One

Start with /v1/ even if you think you won't need it. Retrofitting versioning is painful.

βœ… 2. Document Breaking Changes Clearly

Maintain a CHANGELOG with explicit "BREAKING" labels. Developers need to know what will break.

βœ… 3. Use HTTP Headers for Deprecation

Sunset: Sat, 01 Sep 2026 00:00:00 GMT
Deprecation: true

βœ… 4. Track Version Usage

Monitor which versions are still in use. Identify high-volume clients before deprecation.

βœ… 5. Make Breaking Changes Obvious

Don't hide breaking changes in MINOR or PATCH releases. Bump MAJOR version.

βœ… 6. Provide Migration Guides

Write detailed migration docs with code examples. Show before/after for every change.

βœ… 7. Support 2-3 Versions Maximum

v1 (sunset soon), v2 (current), v3 (beta). More than 3 = maintenance nightmare.

βœ… 8. Use Semantic Versioning for SDKs

API versions: /v1/. SDK versions: npm install api-client@2.3.1

βœ… 9. Test Backward Compatibility

Run v1 integration tests against v2 adapters. Ensure no silent breakage.

βœ… 10. Communicate Early and Often

Announce deprecations 6-12 months in advance. Send emails, blog posts, in-app warnings.

Common Mistakes to Avoid

❌ 1. No Versioning from the Start

Mistake: GET /api/users (no version)

Problem: Can't introduce breaking changes without breaking everyone

Fix: Always start with /v1/

❌ 2. Versioning Everything

Mistake: /v1.2.5/users (too granular)

Problem: Version fatigue, maintenance nightmare

Fix: Major versions only (/v1/, /v2/)

❌ 3. Breaking Changes Without Version Bump

Mistake: Removing fields in v1.1

Problem: Breaks clients who thought v1 was stable

Fix: Breaking change = new major version (v2)

❌ 4. Forcing Immediate Migration

Mistake: "v1 sunset in 30 days"

Problem: Enterprise clients can't move that fast

Fix: 6-12 month deprecation windows

❌ 5. Silent Sunset (No Warning)

Mistake: Shut down v1 without notice

Problem: Breaks production apps, angry developers

Fix: Multi-phase deprecation process

❌ 6. Inconsistent Versioning Across Endpoints

Mistake: /v1/users but /api/v2/orders

Problem: Confusing, hard to document

Fix: Pick one strategy, apply everywhere

❌ 7. No Migration Guide

Mistake: "v2 is out, figure it out yourself"

Problem: Developers won't migrate (too risky)

Fix: Detailed migration docs with examples

❌ 8. Supporting Too Many Versions

Mistake: Maintaining v1, v2, v3, v4, v5 simultaneously

Problem: Security patches need 5x work

Fix: Deprecate aggressively, support 2-3 max

API Versioning Checklist

Before Launching Your API

  • ☐Choose a versioning strategy (URI path recommended)
  • ☐Start with /v1/ even if you think you won't need it
  • ☐Document what constitutes a breaking change
  • ☐Set up version tracking in analytics
  • ☐Write a versioning policy for your team

Before Releasing v2

  • ☐Write comprehensive migration guide with examples
  • ☐Maintain CHANGELOG with all breaking changes listed
  • ☐Set up adapters/transformers between v1 and v2
  • ☐Test backward compatibility (v1 tests pass against v2 adapters)
  • ☐Announce v2 beta to power users for feedback

When Deprecating v1

  • ☐Announce deprecation 6-12 months in advance
  • ☐Add Sunset header to all v1 responses
  • ☐Email all active v1 API users
  • ☐Identify top 20 API consumers and reach out personally
  • ☐Set up deprecation warnings in SDK clients
  • ☐Gradually reduce rate limits 2 months before sunset
  • ☐Monitor support tickets for migration blockers
  • ☐Return 410 Gone on sunset date with migration link

Ongoing Maintenance

  • ☐Track version usage in analytics dashboard
  • ☐Apply security patches to all supported versions
  • ☐Review version support policy quarterly
  • ☐Keep version docs up to date
  • ☐Limit supported versions to 2-3 maximum

Conclusion

API versioning is a contract between you and your developers. Choose a simple, explicit strategy (URI path versioning for most APIs), commit to supporting versions for reasonable timeframes (12-24 months), and communicate changes early and often.

The best versioning strategy is the one you can maintain consistently. Start with /v1/, avoid breaking changes when possible, and when you must break things, give developers plenty of time to migrate.

Key Takeaways

  • βœ… Use URI path versioning (/v1/, /v2/) for simplicity
  • βœ… Expose major versions only (not v1.2.3)
  • βœ… Breaking changes = new version, non-breaking = same version
  • βœ… Support 2-3 versions maximum
  • βœ… Give 6-12 months for migration
  • βœ… Use Sunset and Deprecation headers
  • βœ… Provide detailed migration guides with code examples

Related Guides

πŸ“‘
Recommended

Monitor your APIs and infrastructure in real-time

Better Stack combines uptime monitoring, incident management, and log aggregation. Free tier includes 10 monitors with 3-minute checks.

Try Better Stack Free β†’

πŸ›  Tools We Use & Recommend

Tested across our own infrastructure monitoring 200+ APIs daily

Better StackBest for API Teams

Uptime Monitoring & Incident Management

Used by 100,000+ websites

Monitors your APIs every 30 seconds. Instant alerts via Slack, email, SMS, and phone calls when something goes down.

β€œWe use Better Stack to monitor every API on this site. It caught 23 outages last month before users reported them.”

Free tier Β· Paid from $24/moStart Free Monitoring
1PasswordBest for Credential Security

Secrets Management & Developer Security

Trusted by 150,000+ businesses

Manage API keys, database passwords, and service tokens with CLI integration and automatic rotation.

β€œAfter covering dozens of outages caused by leaked credentials, we recommend every team use a secrets manager.”

SEMrushBest for SEO

SEO & Site Performance Monitoring

Used by 10M+ marketers

Track your site health, uptime, search rankings, and competitor movements from one dashboard.

β€œWe use SEMrush to track how our API status pages rank and catch site health issues early.”

From $129.95/moTry SEMrush Free
View full comparison & more tools β†’Affiliate links β€” we earn a commission at no extra cost to you