Skip to main content
โšก Calmops

API Versioning Strategies: URL, Header, and Query Parameter Approaches

API versioning is crucial for evolving APIs without breaking existing clients. This guide covers different strategies, when to use each, and implementation patterns.

Versioning Strategies Overview

strategies:
  - name: "URL Path"
    example: "/api/v1/users"
    pros: "Simple, visible, cacheable"
    cons: "URL pollution, duplicate endpoints"
    
  - name: "Query Parameter"
    example: "/api/users?version=1"
    pros: "Single endpoint, flexible"
    cons: "Harder to cache, less explicit"
    
  - name: "Header"
    example: "Accept: application/vnd.api.v1+json"
    pros: "Clean URLs, flexible"
    cons: "Less visible, harder to test"
    
  - name: "Content Negotiation"
    example: "Accept: application/vnd.myapp.v1+json"
    pros: "Standard HTTP, clean"
    cons: "Complex, versioning in MIME type"

URL Path Versioning

Implementation

# Flask URL versioning

from flask import Flask, Blueprint, jsonify, request

app = Flask(__name__)

# Version 1 blueprint
v1_bp = Blueprint('v1', __name__, url_prefix='/api/v1')

@v1_bp.route('/users')
def get_users_v1():
    return jsonify({
        'users': [
            {'id': 1, 'name': 'John', 'email': '[email protected]'}
        ]
    })

@v1_bp.route('/users/<int:user_id>')
def get_user_v1(user_id):
    return jsonify({
        'id': user_id,
        'name': 'John',
        'email': '[email protected]'
    })

# Version 2 blueprint
v2_bp = Blueprint('v2', __name__, url_prefix='/api/v2')

@v2_bp.route('/users')
def get_users_v2():
    # More fields in v2
    return jsonify({
        'users': [
            {
                'id': 1,
                'name': 'John',
                'email': '[email protected]',
                'profile': {'avatar': 'url', 'bio': 'Bio'},
                'settings': {'theme': 'dark'}
            }
        ]
    })

# Register blueprints
app.register_blueprint(v1_bp)
app.register_blueprint(v2_bp)

Django REST Framework

# Django URL versioning

from rest_framework import viewsets
from rest_framework.versioning import URLPathVersioning

class UserViewSet(viewsets.ModelViewSet):
    versioning_class = URLPathVersioning
    
    def get_serializer_class(self):
        if self.request.version == 'v1':
            return UserSerializerV1
        return UserSerializerV2

# urls.py
from django.urls import path, include

urlpatterns = [
    path('api/v1/', include((router.urls, 'api'), namespace='v1')),
    path('api/v2/', include((router.urls, 'api'), namespace='v2')),
]

Header Versioning

Custom Header Implementation

# Header-based versioning

from flask import Flask, request, jsonify
from functools import wraps

app = Flask(__name__)

def versionize(versions):
    """Decorator to route to correct version handler"""
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            version = request.headers.get('X-API-Version', 'v1')
            
            version_handler = versions.get(version)
            if not version_handler:
                version_handler = versions.get('v1')  # Default
            
            return version_handler(*args, **kwargs)
        return decorated_function
    return decorator

@app.route('/api/users')
@versionize({
    'v1': lambda: jsonify({'users': [{'name': 'John'}]}),
    'v2': lambda: jsonify({'users': [{'name': 'John', 'avatar': 'url'}]})
})
def get_users():
    pass

Accept Header Versioning

# Using Accept header with MIME types

@app.route('/api/users')
def get_users():
    # Parse Accept header
    accept = request.headers.get('Accept', '')
    
    # Check for versioned media type
    if 'application/vnd.api.v1+json' in accept:
        return jsonify({'version': 'v1', 'data': []})
    elif 'application/vnd.api.v2+json' in accept:
        return jsonify({'version': 'v2', 'data': [], 'metadata': {}})
    
    # Default or fallback
    return jsonify({'version': 'default', 'data': []})


# Client request
headers = {
    'Accept': 'application/vnd.api.v2+json'
}
response = requests.get('/api/users', headers=headers)

Query Parameter Versioning

# Query parameter versioning

@app.route('/api/users')
def get_users():
    version = request.args.get('version', '1')
    
    if version == '1':
        return jsonify({'users': [{'id': 1, 'name': 'John'}]})
    elif version == '2':
        return jsonify({
            'users': [{
                'id': 1,
                'name': 'John',
                'profile': {}
            }]
        })
    
    return jsonify({'error': 'Invalid version'}), 400

Version Deprecation

Handling Deprecated Versions

# Version deprecation handler

from datetime import datetime, timedelta

class VersionDeprecation:
    """Handle API version deprecation"""
    
    def __init__(self):
        self.versions = {
            'v1': {
                'deprecated': True,
                'sunset_date': '2025-06-01',
                'warning_header': 'X-API-Warning'
            },
            'v2': {
                'deprecated': False,
                'sunset_date': None
            }
        }
    
    def check_version(self, version):
        if version not in self.versions:
            return {
                'error': f'Unknown version: {version}',
                'supported': list(self.versions.keys())
            }, 400
        
        info = self.versions[version]
        
        if info['deprecated']:
            sunset = datetime.fromisoformat(info['sunset_date'])
            
            if datetime.now() > sunset:
                return {
                    'error': f'Version {version} no longer supported',
                    'sunset_date': info['sunset_date']
                }, 410  # Gone
        
        return None, None
    
    def add_deprecation_headers(self, response, version):
        info = self.versions.get(version, {})
        
        if info.get('deprecated'):
            response.headers['Deprecation'] = 'true'
            response.headers['Link'] = f'<{self.get_current_version_uri()}>; rel="successor-version"'
            
            if info.get('sunset_date'):
                response.headers['Sunset'] = info['sunset_date']
        
        return response


@app.route('/api/users')
def get_users():
    version = request.args.get('version', 'v2')
    
    checker = VersionDeprecation()
    error, status = checker.check_version(version)
    
    if error:
        return jsonify(error), status
    
    # Get data...
    response = jsonify({'users': []})
    return checker.add_deprecation_headers(response, version)

Best Practices

# API versioning best practices

when_to_version:
  - "Breaking changes to response format"
  - "Removing fields"
  - "Changing field types"
  - "Adding required fields"

strategies:
  - "URL path: Most explicit, good for public APIs"
  - "Header: Clean URLs, good for private APIs"
  - "Query: Simple but less visible"

deprecation:
  - "Announce deprecations early"
  - "Use Sunset header"
  - "Provide migration guide"
  - "Set timeline (6-12 months)"

advice:
  - "Don't version on every change"
  - "Use semantic versioning"
  - "Document supported versions"
  - "Monitor version usage"

Conclusion

Choose versioning strategy based on your needs:

  • URL path: Most visible, good for public APIs
  • Header: Clean URLs, flexible
  • Query parameter: Simple but less explicit

Always plan for deprecation and communicate changes early.


Comments