Advanced20 min read

API Versioning & Evolution

Learn how to version APIs properly, handle breaking changes gracefully, and evolve your API without breaking existing clients.

Why Version APIs?

APIs are contracts between your server and your clients. When you release an API, developers integrate it into their applications. They write code that depends on your endpoints, request formats, and response structures. If you change the API in a way that breaks their code, you break their applications.

This is the fundamental problem API versioning solves: How do you improve and evolve your API without breaking existing clients?

There are two types of changes:

Non-breaking changes (safe):

  • Adding a new endpoint
  • Adding optional fields to requests
  • Adding new fields to responses (clients should ignore unknown fields)
  • Making required fields optional
  • Changing internal implementation without affecting contracts

These changes are backwards compatible. Existing clients continue to work exactly as before. You can deploy these changes without versioning.

Breaking changes (dangerous):

  • Removing or renaming an endpoint
  • Removing or renaming a field in the response
  • Changing the data type of a field ("id": "123""id": 123)
  • Making an optional field required
  • Changing authentication methods
  • Changing error response formats

These changes will break existing clients. A mobile app using your v1 API will crash if you suddenly rename /users to /accounts or change user.name to user.fullName.

Real-world example: In 2012, Instagram changed their API to require access tokens for all requests. Apps that were working suddenly stopped. Developers were furious. Instagram learned to version their API properly.

The solution is versioning. When you need to make breaking changes, you create a new version of the API while keeping the old version running. Existing clients continue using v1, new clients can adopt v2, and everyone is happy. Eventually, after giving developers months of notice, you can deprecate and shut down v1.

Versioning Strategies

There are three main strategies for versioning APIs, each with trade-offs:

1. URL Path Versioning (Most Popular)

Include the version in the URL path:

  • https://api.example.com/v1/users
  • https://api.example.com/v2/users
  • https://api.example.com/v3/users

Pros:

  • Extremely explicit and visible
  • Easy to route in frameworks (different route handlers for v1 vs v2)
  • Works perfectly with browser testing (just type the URL)
  • Caching is straightforward (v1 and v2 are different URLs)

Cons:

  • Clutters URLs
  • Can lead to duplicate code if not careful

This is the most common approach. Used by Stripe, Twitter, GitHub, and most major APIs.

2. Query Parameter Versioning

Include the version as a query parameter:

  • https://api.example.com/users?version=1
  • https://api.example.com/users?v=2

Pros:

  • Keeps the base URL clean
  • Easy to add to existing APIs

Cons:

  • Easy to forget (requires discipline)
  • Caching is harder (cache keys must include the query param)
  • Less visible than path versioning

Used by Google Maps API and some older APIs.

3. Header Versioning (RESTful Purist)

Specify the version in an HTTP header:

  • Accept: application/vnd.example.v1+json
  • API-Version: 2

Pros:

  • Keeps URLs clean and semantic
  • Follows HTTP standards (content negotiation)
  • Version is metadata, not part of the resource identifier

Cons:

  • Not visible in the URL (harder to test in browser)
  • Requires custom headers (more work for clients)
  • Caching requires Vary: Accept header configuration

Used by GitHub API (Accept: application/vnd.github.v3+json) and Azure.

Which to choose?

For most APIs, URL path versioning is the best choice. It is simple, explicit, and universally supported. Reserve header versioning for situations where you need fine-grained content negotiation (e.g., supporting both JSON and XML formats). Avoid query parameter versioning unless you have a specific reason.

Semantic Versioning for APIs

Semantic Versioning (semver) is a versioning scheme that uses three numbers: MAJOR.MINOR.PATCH (e.g., 2.4.1). While it was designed for software libraries, the principles apply to APIs.

Here is how it maps:

MAJOR version — Increment when you make breaking changes

  • Removing or renaming endpoints
  • Changing response structures
  • Changing authentication requirements
  • Example: v1 → v2

MINOR version — Increment when you add backwards-compatible functionality

  • Adding new endpoints
  • Adding optional parameters
  • Adding new fields to responses
  • Example: v2.3 → v2.4

PATCH version — Increment for bug fixes that don't change the API contract

  • Fixing incorrect response data
  • Fixing performance issues
  • Fixing security vulnerabilities
  • Example: v2.4.1 → v2.4.2

Most public APIs only expose the MAJOR version in the URL (/v2/) and handle minor/patch updates transparently. Clients get bug fixes and new features automatically without needing to change their code.

Example from Stripe:

  • Stripe's API is versioned by date: 2023-10-16, 2024-01-01, etc.
  • Each date represents a snapshot of the API at that point in time
  • Stripe can make breaking changes, but existing clients stay pinned to their version
  • Clients opt-in to new versions by updating their API version header

This date-based versioning is a variation of semantic versioning. Each date is like a major version. Stripe can evolve the API rapidly while giving developers complete control over when they upgrade.

Key principle: Your versioning strategy should give clients stability (existing code keeps working) and control (they choose when to upgrade). Never force breaking changes on clients without warning and a migration path.

Deprecation and Backwards Compatibility

Deprecation is the process of phasing out old API versions while giving developers time to migrate. Done well, it is smooth. Done poorly, it creates chaos and angry developers.

Best practices for deprecation:

1. Give plenty of notice Announce deprecation at least 6-12 months before shutdown. Major APIs (Google, Stripe) often give 1-2 years. Send emails, update documentation, add warnings in the developer portal.

2. Use the Sunset header Include a Sunset HTTP header in responses to indicate when the endpoint will stop working:

Sunset: Sat, 31 Dec 2025 23:59:59 GMT

This is an official HTTP standard (RFC 8594). Clients can parse this header and warn developers.

3. Provide migration guides Do not just say "v1 is deprecated, use v2." Show developers exactly what changed, how to migrate, and provide code examples. Compare old vs new side-by-side.

4. Add deprecation warnings in responses Include a custom header or field in the JSON response:

json
{
  "data": { ... },
  "warnings": [
    {
      "type": "deprecation",
      "message": "This endpoint is deprecated and will be removed on 2025-12-31. Migrate to /v2/users.",
      "migration_guide": "https://docs.example.com/migrate-to-v2"
    }
  ]
}

5. Monitor usage Track how many clients are still using deprecated endpoints. Before shutting down v1, ensure usage is near zero. Reach out to heavy users personally.

Backwards compatibility strategies:

  • Additive changes only: Add new fields, don't remove or rename existing ones
  • Graceful degradation: If a client sends an old request format, transform it internally to the new format
  • Default values: Make new required fields optional with sensible defaults
  • Adapter pattern: Keep v1 as a thin adapter over v2 (transform requests/responses)

Remember: every breaking change costs your users time and money. Treat deprecation seriously. The best API is one that evolves without breaking existing code.

Real-World Versioning Examples

html
<h3 style="font-family:sans-serif; margin-bottom:15px;">API Versioning in the Wild</h3>

<div style="font-family:sans-serif; line-height:1.6;">
  <div style="background:#f8fafc; padding:15px; border-left:4px solid #3b82f6; margin-bottom:15px;">
    <h4 style="margin:0 0 8px 0; color:#1e293b;">Stripe (Date-based)</h4>
    <code style="background:#e2e8f0; padding:4px 8px; border-radius:4px; display:block; margin-bottom:8px;">
      curl https://api.stripe.com/v1/charges \<br>
      &nbsp;&nbsp;-H "Stripe-Version: 2024-01-01"
    </code>
    <p style="margin:0; font-size:14px; color:#475569;">
      Each date is a version. Clients pin to a date, Stripe can make breaking changes without affecting them.
    </p>
  </div>

  <div style="background:#f8fafc; padding:15px; border-left:4px solid #10b981; margin-bottom:15px;">
    <h4 style="margin:0 0 8px 0; color:#1e293b;">GitHub (Header + Path)</h4>
    <code style="background:#e2e8f0; padding:4px 8px; border-radius:4px; display:block; margin-bottom:8px;">
      curl https://api.github.com/repos/user/repo \<br>
      &nbsp;&nbsp;-H "Accept: application/vnd.github.v3+json"
    </code>
    <p style="margin:0; font-size:14px; color:#475569;">
      Uses v3 in the Accept header. Elegant but requires custom header support.
    </p>
  </div>

  <div style="background:#f8fafc; padding:15px; border-left:4px solid #f59e0b;">
    <h4 style="margin:0 0 8px 0; color:#1e293b;">Twitter (URL Path)</h4>
    <code style="background:#e2e8f0; padding:4px 8px; border-radius:4px; display:block; margin-bottom:8px;">
      GET https://api.twitter.com/2/tweets/1234567890
    </code>
    <p style="margin:0; font-size:14px; color:#475569;">
      Simple URL path versioning. v1 was shut down in 2013, v2 launched in 2020.
    </p>
  </div>
</div>

Which of the following is a breaking change that requires a new API version?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.