API Versioning Strategies That Work

May 29, 2017

APIs are contracts. Once published, they’re hard to change. Add a field, and existing clients work fine. Remove a field, and existing clients break. The challenge: evolve your API to meet new requirements without breaking the consumers who depend on current behavior.

Versioning provides the solution—multiple API versions coexisting, enabling evolution while maintaining compatibility.

Versioning Strategies

URL Path Versioning

Include version in the URL path:

GET /v1/users/123
GET /v2/users/123

Advantages:

Disadvantages:

URL versioning is the most common approach because of its clarity.

Query Parameter Versioning

Include version as query parameter:

GET /users/123?version=1
GET /users/123?version=2

Advantages:

Disadvantages:

Header Versioning

Include version in request headers:

GET /users/123
Accept: application/vnd.myapi.v1+json

Or custom header:

GET /users/123
API-Version: 1

Advantages:

Disadvantages:

No Explicit Versioning

Make all changes backward compatible. Never break existing clients.

Advantages:

Disadvantages:

Works for simple APIs with limited evolution needs. Becomes difficult as APIs mature.

What Constitutes a Breaking Change

Understanding what breaks clients helps avoid accidental breakage.

Breaking Changes

Non-Breaking Changes

Gray Areas

Document your compatibility policy so consumers know what to expect.

Deprecation Strategy

Deprecation bridges old and new versions:

Mark Deprecated

Signal that features will be removed:

{
  "user_id": 123,
  "email": "user@example.com",
  "legacyId": "abc123"  // Deprecated, use user_id
}

Response headers can indicate deprecation:

Deprecation: true
Sunset: Sat, 31 Dec 2017 23:59:59 GMT
Link: </docs/migration>; rel="deprecation"

Migration Period

Give consumers time to migrate:

  1. Announce: Communicate deprecation with timeline
  2. Overlap: Run old and new simultaneously
  3. Monitor: Track usage of deprecated features
  4. Communicate: Notify heavy users directly
  5. Remove: Only after migration period ends

Migration periods depend on your API’s consumer profile. Public APIs need longer periods than internal APIs.

Sunset Policy

Document your sunset policy:

Clear policy sets expectations and builds trust.

Implementation Patterns

Version per Route

Different route handlers for each version:

@app.route('/v1/users/<id>')
def get_user_v1(id):
    return UserV1Schema().dump(get_user(id))

@app.route('/v2/users/<id>')
def get_user_v2(id):
    return UserV2Schema().dump(get_user(id))

Simple but duplicates code.

Transformation Layer

Single implementation with version-specific transformation:

@app.route('/v<version>/users/<id>')
def get_user(version, id):
    user = get_user_from_db(id)
    return transform_for_version(user, version)

def transform_for_version(user, version):
    if version == '1':
        return UserV1Schema().dump(user)
    elif version == '2':
        return UserV2Schema().dump(user)

Centralizes logic but transformation layer can become complex.

Feature Flags

Version-like behavior via feature flags:

def get_user(id, include_new_fields=False):
    user = get_user_from_db(id)
    response = basic_user_schema(user)

    if include_new_fields:
        response['new_field'] = user.new_field

    return response

More granular than version numbers but can become complex.

Best Practices

Version Early

Don’t wait until you need breaking changes to think about versioning. Include version from the start, even if you only have v1.

Document Compatibility

Be explicit about what compatibility guarantees you provide:

Monitor Version Usage

Track which versions consumers use:

Data informs deprecation decisions.

Provide Migration Guides

When releasing new versions, provide:

Good documentation smooths migration.

Limit Active Versions

Supporting many versions simultaneously is expensive. Have a policy:

Test Across Versions

Test that all supported versions work correctly. Version-specific bugs erode trust.

Key Takeaways