Skip to content

API Versioning Strategies — URI, Header, and Query Parameter Approaches

DodaTech Updated 2026-06-22 10 min read

In this tutorial, you'll learn about API Versioning Strategies. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

API Versioning is the practice of managing changes to your API over time without breaking existing clients, allowing different clients to use different versions of the same endpoint simultaneously.

What You'll Learn

By the end of this tutorial, you will implement four API Versioning strategies (URI path, header, query parameter, media type), choose the right approach for your API, design backward-compatible changes, and deprecate old versions gracefully.

Why It Matters

Without versioning, every API change risks breaking mobile apps, third-party integrations, and internal services that depend on the existing contract. Doda Browser maintains API version compatibility for its public extension API, ensuring thousands of extensions continue to work across updates.

Real-World Use

Stripe's API uses URL versioning (/v1/charges, /v2/charges) with a deprecation timeline of 2+ years. Clients opt into new versions explicitly, and old versions continue working for existing integrations without forced Migration.

Versioning Strategy Comparison

Graph LR
    subgraph "API Gateway"
        A[Request] --> B{Routing}
    end
    B --> C["/v1/users/"]
    B --> D["/v2/users/"]
    B --> E["Accept: app/vnd.API.v2+JSON"]
    B --> F["?version=2"]
    C --> G[V1 Handler]
    D --> H[V2 Handler]
    E --> H
    F --> G
    style B fill:#f90,color:#fff

Different clients can target different API versions simultaneously. The versioning strategy determines how clients specify which version they want.

Table: Versioning Strategies

Strategy Example Pros Cons
URI Path /v1/users Simple, explicit, cacheable URL pollution, hard to redirect
Header Accept: vnd.myapp.v2+json Clean URLs, RESTful Harder to test, requires headers
Query Param /users?version=2 Easy to implement Caching issues, not RESTful
Media Type application/v1+json Standards-based Complex, requires parsing
Subdomain v1.api.example.com Full isolation DNS overhead, SSL certs

Python: URI Versioning with Flask Blueprint

# API_v1.py
# API version 1 endpoints
from Flask import Blueprint, jsonify

API_v1 = Blueprint('API_v1', __name__, URL_prefix='/API/v1')

users_db = [
    {'id': 1, 'name': 'Alice', 'email': 'alice"@example".com'},
    {'id': 2, 'name': 'Bob', 'email': 'bob"@example".com'},
]

@API_v1.route('/users')
def list_users():
    """V1: Returns basic user information."""
    return jsonify({'users': users_db, 'version': '1.0'})

@API_v1.route('/users/<int:user_id>')
def get_user(user_id):
    """V1: Returns user without profile fields."""
    user = next((u for u in users_db if u['id'] == user_id), None)
    if not user:
        return jsonify({'error': 'User not found'}), 404
    return jsonify({'user': user, 'version': '1.0'})
# API_v2.py
# API version 2 endpoints with enhanced data
from Flask import Blueprint, jsonify

API_v2 = Blueprint('API_v2', __name__, URL_prefix='/API/v2')

users_db_v2 = [
    {
        'id': 1, 'name': 'Alice', 'email': 'alice"@example".com',
        'profile': {'bio': 'Developer', 'avatar_URL': '/avatars/alice.jpg'},
        'created_at': '2026-01-15T10:00:00Z',
        'updated_at': '2026-06-20T15:30:00Z',
    },
    {
        'id': 2, 'name': 'Bob', 'email': 'bob"@example".com',
        'profile': {'bio': 'Designer', 'avatar_URL': '/avatars/bob.jpg'},
        'created_at': '2026-02-01T08:00:00Z',
        'updated_at': '2026-06-19T12:00:00Z',
    },
]

@API_v2.route('/users')
def list_users():
    """V2: Returns users with profile and timestamps."""
    return jsonify({
        'data': users_db_v2,
        'meta': {'version': '2.0', 'count': len(users_db_v2)},
    })

@API_v2.route('/users/<int:user_id>')
def get_user(user_id):
    """V2: Returns user with profile fields and audit timestamps."""
    user = next((u for u in users_db_v2 if u['id'] == user_id), None)
    if not user:
        error_response = {
            'error': {
                'code': 'NOT_FOUND',
                'message': 'User not found',
                'request_id': 'req-abc-123',
            }
        }
        return jsonify(error_response), 404
    return jsonify({'data': user, 'meta': {'version': '2.0'}})
# app.py
# Flask app with versioned routes
from Flask import Flask, request, jsonify
from API_v1 import API_v1
from API_v2 import API_v2

app = Flask(__name__)
app.Register_blueprint(API_v1)
app.Register_blueprint(API_v2)

@app.route('/API/version')
def get_versions():
    """Return supported API versions."""
    return jsonify({
        'versions': ['v1', 'v2'],
        'latest': 'v2',
        'deprecated': ['v1'],
        'deprecation_date': '2027-01-01',
        'sunset_date': '2027-06-30',
    })

if __name__ == '__main__':
    app.run(debug=True)

Expected output:

// GET /API/v2/users
{
  "data": [{"id": 1, "name": "Alice", "profile": {...}, ...}],
  "meta": {"version": "2.0", "count": 2}
}

Node.js: Header-Based Versioning

// header-versioning.js
// Express middleware for Accept header versioning
const Express = require('Express');
const app = Express();

const versionRegex = /^application\/vnd\.myapp\.v(\d+)\+JSON$/;

function versionMiddleware(req, res, next) {
  const accept = req.headers['accept'] || '';
  const match = accept.match(versionRegex);

  if (match) {
    req.apiVersion = parseInt(match[1], 10);
  } else {
    // Default to latest version
    req.apiVersion = 2;
  }

  // Validate version
  if (req.apiVersion < 1 || req.apiVersion > 2) {
    return res.status(400).JSON({
      error: 'Unsupported API version',
      supportedVersions: ['v1', 'v2'],
    });
  }

  next();
}

app.use(versionMiddleware);

// Version-aware route handler
app.get('/users', (req, res) => {
  if (req.apiVersion === 1) {
    return res.JSON({
      users: [
        { id: 1, name: 'Alice', email: 'alice"@example".com' },
      ],
      version: '1.0',
    });
  }

  // Version 2 (default)
  res.JSON({
    data: [
      {
        id: 1, name: 'Alice', email: 'alice"@example".com',
        profile: { bio: 'Developer' },
      },
    ],
    meta: { version: '2.0' },
  });
});

app.listen(3000);

Deprecation Handling

# deprecation.py
# API deprecation middleware
from Flask import request, g, jsonify
from datetime import datetime, date
import functools
import warnings

DEPRECATED_VERSIONS = {
    'v1': {
        'sunset': date(2027, 6, 30),
        'Migration_URL': '/docs/Migration-v1-to-v2',
        'changelog': '/docs/changelog#v2',
    }
}

def deprecation_check():
    """Add deprecation warnings to response headers."""
    version = request.path.split('/')[2] if '/API/' in request.path else None

    if version and version in DEPRECATED_VERSIONS:
        info = DEPRECATED_VERSIONS[version]
        days_left = (info['sunset'] - date.today()).days

        g.deprecation_headers = {
            'Sunset': info['sunset'].isoformat(),
            'Deprecation': f'True; sunset="{info['sunset']}"; '
                          f'days_remaining={days_left}',
            'Link': f'<{info["Migration_URL"]}>; rel="deprecation"',
        }
    else:
        g.deprecation_headers = {}

@app.after_request
def add_deprecation_headers(response):
    """Attach deprecation headers to response."""
    if hasattr(g, 'deprecation_headers'):
        for key, value in g.deprecation_headers.items():
            response.headers[key] = value
    return response

def deprecated(version, alternative):
    """Decorator marking an endpoint as deprecated."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            warnings.warn(
                f"Endpoint '{func.__name__}' is deprecated in {version}. "
                f"Use {alternative} instead.",
                DeprecationWarning, stacklevel=2
            )
            return func(*args, **kwargs)
        return wrapper
    return decorator

# Usage
@deprecated(version='v1', alternative='/API/v2/users')
def old_users_endpoint():
    return jsonify({'users': []})

Common Errors

1. Removing Fields Without Notice

Removing a field from a response breaks clients that depend on it. Use additive changes: add fields but never remove or rename them within the same version. Deprecate fields with a deprecated annotation before removing them in a new version.

2. Changing Error Response Format

Clients parse error responses programmatically. Changing the error schema (field names, structure, status codes) between versions is a breaking change. Maintain consistent error formats within a version.

3. No Version Discovery Endpoint

Clients need a way to discover available versions, their status (current/deprecated/sunset), and Migration guides. Provide a /api/version or /api/versions endpoint that returns this metadata.

4. Breaking Changes in Patch Versions

Semantic versioning for APIs means breaking changes (field removal, type changes) only occur in major versions. Patch and minor versions must be backward-compatible. Non-breaking changes are: adding optional fields, adding new endpoints, and expanding enum values.

5. Not Versioning the Database Schema

When API v2 introduces new fields, the database schema must support both v1 and v2 responses. Use database views, computed columns, or transformation layers to serve old formats without duplicating data.

6. Ignoring Client Identification

Without knowing which client version is calling, you cannot enforce deprecation timelines. Require a User-Agent or X-Client-Version header and log it for analytics. Use this data to decide when to sunset old versions.

Practice Questions

1. Which API Versioning strategy is most commonly used?

URI path versioning (/v1/resource) is the most common because it is explicit, easy to test, cacheable, and visible in every request. It is supported by all HTTP clients without special header configuration.

2. How do you handle breaking changes without versioning?

Add new optional fields without modifying existing ones. Use accept headers for content negotiation within the same endpoint. Add new endpoints instead of changing existing ones. Deprecate old behavior gradually.

3. What is a sunset header and why is it important?

The Sunset HTTP header tells clients when a version will stop being supported. It gives clients a clear deadline for Migration. Combined with the Deprecation header, it provides a complete deprecation timeline.

4. How long should you support old API versions?

Industry standard is 6-24 months depending on the client BASE. Mobile apps update slowly (some users stay on old versions for years). Enterprise APIs often support versions for 2+ years. Communicate deprecation timelines clearly.

5. Challenge: Design an API Versioning system for a social media platform where v1 returns basic user profiles (name, email) and v2 adds profile pictures, bio, followers count, and posts. Implement URI path versioning with Flask or Express. Add a deprecation middleware that returns Sunset headers for v1. Create a version discovery endpoint. Write a Migration guide for v1 to v2 that explains field changes, new error format, and authentication updates.

Mini Project: Version-Aware API Gateway

// API-gateway.js
// Simple API Gateway with version routing
const Express = require('Express');
const app = Express();

const versions = {
  v1: {
    active: true,
    deprecationDate: '2027-01-01',
    sunsetDate: '2027-06-30',
  },
  v2: {
    active: true,
    deprecationDate: null,
    sunsetDate: null,
  },
};

function versionRouter(req, res, next) {
  const [_, version, ...REST] = req.path.split('/');
  // version = 'v1' or 'v2'

  if (!version || !versions[version]) {
    return res.status(404).JSON({
      error: 'Unknown API version',
      available: Object.keys(versions),
    });
  }

  if (!versions[version].active) {
    return res.status(410).JSON({
      error: `Version ${version} is no longer supported`,
      sunsetDate: versions[version].sunsetDate,
    });
  }

  // Attach version info to request
  req.apiVersion = version;
  req.apiInfo = versions[version];

  // Add deprecation headers
  if (versions[version].deprecationDate) {
    res.set('Deprecation', `True; sunset="${versions[version].sunsetDate}"`);
    res.set('Sunset', versions[version].sunsetDate);
    res.set('Link', '</docs/Migration>; rel="deprecation"');
  }

  next();
}

app.use('/API', versionRouter);

// Version-agnostic route
app.get('/API/users', (req, res) => {
  if (req.apiVersion === 'v1') {
    return res.JSON({
      users: [{ id: 1, name: 'Alice', email: 'alice"@example".com' }],
      version: '1.0',
    });
  }
  res.JSON({
    data: [{ id: 1, name: 'Alice', profile: { bio: 'Developer' } }],
    meta: { version: '2.0' },
  });
});

// Health endpoint
app.get('/API/version', (req, res) => {
  res.JSON({
    latest: 'v2',
    versions,
    documentation: '/docs/API',
  });
});

app.listen(3000);

FAQ

Should I version my API from day one? Yes. Even if you only have one version, use `/v1/` in your URLs from the start. Adding versioning later requires migrating all existing clients. It is much harder to add versioning retroactively than to plan for it upfront.

How do I test multiple API versions? Maintain separate test suites for each version. The tests should cover the same scenarios but expect different response formats. Use Contract Testing to verify that changes to a shared data layer do not break older version responses.

Can I use different authentication for different versions? Avoid it. Authentication should be consistent across versions. Changing auth between versions creates confusion and security gaps. Keep auth logic version-agnostic and handle it before version routing.

What is the cost of maintaining multiple versions? Each version increases the surface area for bugs, testing effort, and developer cognitive load. Limit active versions to 2-3 at any time. Have a clear deprecation policy and sunset old versions on schedule.

How do I handle versioning for WebSocket APIs? Include the version in the WebSocket URL path (/v1/ws/chat). Negotiate the version during the handshake. Use a message envelope that includes the protocol version in each message frame for long-lived connections.

Related Concepts

Microservices Communication
GraphQL vs REST
API Gateway

What's Next

You now understand API Versioning strategies. Next, learn about API Gateway patterns for routing versioned traffic, then explore REST API design for designing versioned resources.

  • Practice daily — Add versioning to an existing unversioned API using URI path strategy
  • Build a project — Build a version-aware API Gateway with version discovery, deprecation headers, and request routing
  • Explore related topics — Check out API changelog management and semantic versioning for APIs

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro