Skip to content

Query Parameter API Versioning — Complete Guide

DodaTech Updated 2026-06-28 4 min read

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

Query parameter versioning specifies the API version through a query string parameter like ?version=2 or ?api_version=v2 in the URL.

What You'll Learn

By the end of this lesson, you will implement query parameter versioning, understand its Caching implications, and know when it is appropriate.

Why It Matters

Query parameter versioning is the easiest Strategy to implement and does not require URL path changes, making it attractive for simple APIs.

Real-World Use

Some APIs use ?v=2 as a query parameter for versioning, especially when retrofitting versioning onto existing unversioned endpoints.

Query Parameter Versioning Flow

flowchart LR
    Request["GET /users?version=2"] --> Parse[Parse ?version=]
    Parse -->|version=1| V1[V1 Handler]
    Parse -->|version=2| V2[V2 Handler]
    Parse -->|missing| Default[Default Version]

Implementation

# query_version.py
from typing import Any, Dict, Optional

class QueryVersionHandler:
    def __init__(self, param_name: str = "version", default: int = 1):
        self.param_name = param_name
        self.default = default
        self.handlers: Dict[int, callable] = {}

    def add_handler(self, version: int, handler: callable):
        self.handlers[version] = handler

    def handle(self, query_params: Dict[str, str], **kwargs) -> Dict:
        version_str = query_params.get(self.param_name)
        version = self.default

        if version_str:
            try:
                version = int(version_str)
            except (ValueError, TypeError):
                return {"error": f"Invalid version: {version_str}"}

        handler = self.handlers.get(version)
        if not handler:
            return {"error": f"Version {version} not supported",
                    "supported": list(self.handlers.keys())}

        return handler(**kwargs)

handler = QueryVersionHandler(param_name="version", default=1)

def v1_users():
    return {"users": [{"id": 1, "name": "Alice"}], "version": "v1"}

def v2_users():
    return {"data": [{"id": 1, "name": "Alice", "role": "admin"}],
            "meta": {"version": "v2"}}

handler.add_handler(1, v1_users)
handler.add_handler(2, v2_users)

tests = [{"version": "2"}, {"version": "1"}, {}, {"version": "invalid"}, {"version": "3"}]
for params in tests:
    result = handler.handle(params)
    print(f"query: {params} -> {result.get('meta', result).get('version', result.get('error', '?'))}")

Expected output:

query: {'version': '2'} -> v2
query: {'version': '1'} -> v1
query: {} -> v1
query: {'version': 'invalid'} -> Invalid version: invalid
query: {'version': '3'} -> Version 3 not supported

Caching Implications

Query parameter versioning has significant caching implications:

  • CDNs may cache the unversioned URL first, serving wrong version to subsequent clients
  • Query parameters are often ignored in cache keys by default
  • Solution: include version parameter in cache key configuration
# query_cache_issue.py
from typing import Dict, Optional

class CacheKeyBuilder:
    def __init__(self):
        self.version_param = "version"

    def build(self, path: str, query: Dict) -> str:
        version = query.get(self.version_param, "1")
        return f"{path}:v{version}"

builder = CacheKeyBuilder()
urls = [
    ("/api/users", {"version": "1"}),
    ("/api/users", {"version": "2"}),
    ("/api/users", {}),
]
for path, query in urls:
    key = builder.build(path, query)
    print(f"Cache key: {key}")

Expected output:

Cache key: /api/users:v1
Cache key: /api/users:v2
Cache key: /api/users:v1

Common Mistakes

1. Cache Poisoning

Without version in cache keys, one version's response can be served to clients requesting another version. Always include version in cache keys.

2. Not Handling Missing Parameter

Always default to a version when the parameter is missing. Document which version is the default.

3. Conflating with Resource Parameters

Using ?version=2 alongside ?page=2&limit=10 can be confusing. Use a distinct parameter name.

4. Allowing Override via Body

Clients should not be able to override the version in POST bodies. Version should come from the URL/query only.

5. No Validation of Version Values

Accept only valid version numbers. Return 400 for invalid values rather than silently defaulting.

Practice Questions

1. How does query parameter versioning work?

The version is specified as a query string parameter: GET /users?version=2.

2. What is the main caching problem with this approach?

CDNs and proxies may not include query parameters in cache keys, potentially serving wrong versions.

3. When is query parameter versioning acceptable?

For internal APIs, development environments, or when retrofitting versioning onto existing unversioned endpoints.

4. What is the difference between ?version=2 and /v2/users?

URI versioning has unique URLs for each version (good for caching). Query versioning shares the same URL (problematic for caching).

Challenge

Implement query parameter versioning for a blog API where v1 returns posts with author name, v2 adds category and tags, and v3 adds pagination metadata.

FAQ

Is query parameter versioning RESTful?

Most REST practitioners consider it less RESTful because query parameters should filter resources, not select API versions.

Does Google index different query versions separately?

Yes, Google treats ?version=1 and ?version=2 as different URLs. Use canonical tags to avoid duplication.

Can I use query versioning with POST requests?

Yes, query parameters work with any HTTP method. For POST, put the version in the URL, not the body.

How do I configure CDN caching for query versioning?

Configure your CDN to include the version parameter in the cache key. CloudFront, Cloudflare, and Fastly all support this.

What if a client sends multiple version parameters?

Use the first or last occurrence consistently. Document which one takes precedence.

Mini Project: Query Version Router

# query_router.py
from typing import Dict, Optional

class QueryVersionRouter:
    def __init__(self, param: str = "version", default: int = 1):
        self.param = param
        self.default = default
        self.versions = {}

    def register(self, version: int, handler):
        self.versions[version] = handler

    def route(self, query: Dict) -> Dict:
        try:
            v = int(query.get(self.param, self.default))
        except (TypeError, ValueError):
            return {"status": 400, "error": "Invalid version"}
        if v not in self.versions:
            return {"status": 400, "error": f"Version {v} not supported",
                    "supported": list(self.versions.keys())}
        return {"status": 200, "data": self.versions[v](), "version": f"v{v}"}

r = QueryVersionRouter()
r.register(1, lambda: {"msg": "v1 response"})
r.register(2, lambda: {"msg": "v2 response", "extra": True})

print(r.route({"version": "2"}))
print(r.route({"version": "1"}))
print(r.route({}))

Expected output:

{'status': 200, 'data': {'msg': 'v2 response', 'extra': True}, 'version': 'v2'}
{'status': 200, 'data': {'msg': 'v1 response'}, 'version': 'v1'}
{'status': 200, 'data': {'msg': 'v1 response'}, 'version': 'v1'}

What's Next

You understand query parameter versioning. Next, learn about content negotiation, then explore media type versioning.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro