Custom Version Resolver
By default, django-ninja-crane detects the API version from the HTTP header (X-API-Version).
You can customize this to use subdomains, URL paths, cookies, query parameters, or any other mechanism by subclassing VersionedAPIMiddleware.
Subclassing the Middleware
Override the _extract_version method to implement your custom logic:
from crane.middleware import VersionedAPIMiddleware
class MyVersionMiddleware(VersionedAPIMiddleware):
def _extract_version(self, request, api) -> str:
# Your custom logic here
# Priority: subdomain > header > cookie > default
version = self._from_subdomain(request)
if version:
return version
version = request.headers.get(api.version_header)
if version:
return version
version = request.COOKIES.get("api_version")
if version:
return version
return api.default_version
def _from_subdomain(self, request) -> str | None:
host = request.get_host()
if host.startswith("v"):
return host.split(".")[0][1:]
return None
Register in settings:
Version Validation
The middleware validates that the requested version exists. If you want custom validation:
class StrictVersionMiddleware(VersionedAPIMiddleware):
def _resolve_version(self, version: str, ctx) -> str | None:
# Only allow specific versions
allowed = {"1", "2", "latest"}
if version not in allowed:
return None
return super()._resolve_version(version, ctx)
Common Patterns
Subdomain Versioning
Extract version from subdomain like v1.api.example.com:
class SubdomainVersionMiddleware(VersionedAPIMiddleware):
def _extract_version(self, request, api) -> str:
host = request.get_host()
if host.startswith("v") and "." in host:
return host.split(".")[0][1:] # "v1.api.example.com" → "1"
return super()._extract_version(request, api)
URL Path Versioning
Extract version from URL path like /api/v1/users:
class PathVersionMiddleware(VersionedAPIMiddleware):
def _extract_version(self, request, api) -> str:
path = request.path
for part in path.split("/"):
if part.startswith("v") and part[1:].isdigit():
return part[1:] # "/api/v1/users" → "1"
return super()._extract_version(request, api)
Warning
URL path versioning requires careful URL configuration. Each version path must route to the same API, and you may need to strip the version prefix before processing.
Cookie-Based Versioning
Store the version preference in a cookie (useful for browser clients):
class CookieVersionMiddleware(VersionedAPIMiddleware):
def _extract_version(self, request, api) -> str:
version = request.COOKIES.get("api_version")
if version:
return version
return super()._extract_version(request, api)
User-Based Versioning
Different users on different versions (assuming your user model has an api_version attribute).
class UserVersionMiddleware(VersionedAPIMiddleware):
def _extract_version(self, request, api) -> str:
if hasattr(request, 'user') and request.user.is_authenticated:
version = getattr(request.user, 'api_version', None)
if version:
return version
return super()._extract_version(request, api)
Combining Multiple Strategies
You can combine strategies with fallback:
class FlexibleVersionMiddleware(VersionedAPIMiddleware):
def _extract_version(self, request, api) -> str:
# Try each strategy in order
strategies = [
self._from_subdomain,
self._from_cookie,
self._from_user,
]
for strategy in strategies:
version = strategy(request)
if version:
return version
# Fall back to default header-based extraction
return super()._extract_version(request, api)
def _from_subdomain(self, request) -> str | None:
host = request.get_host()
if host.startswith("v") and "." in host:
return host.split(".")[0][1:]
return None
def _from_cookie(self, request) -> str | None:
return request.COOKIES.get("api_version")
def _from_user(self, request) -> str | None:
if hasattr(request, 'user') and request.user.is_authenticated:
return getattr(request.user, 'api_version', None)
return None