Skip to main content

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

Created: February 21, 2026 Larry Qu 4 min read

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

Share this article

Scan to read on mobile