Versioning in Django REST Framework
Project Setup
Create a Django project named Myproject with an app named api.
api/models.py
from django.db import models
class UserGroup(models.Model):
title = models.CharField(max_length=32)
class UserInfo(models.Model):
user_type_choices = (
(1, 'Standard User'),
(2, 'VIP'),
(3, 'SVIP'),
)
user_type = models.IntegerField(choices=user_type_choices)
username = models.CharField(max_length=32, unique=True)
password = models.CharField(max_length=64)
group = models.ForeignKey("UserGroup")
roles = models.ManyToManyField("Role")
class UserToken(models.Model):
user = models.OneToOneField(to='UserInfo')
token = models.CharField(max_length=64)
class Role(models.Model):
title = models.CharField(max_length=32)
Myproject/urls.py
from django.urls import path, include
urlpatterns = [
path('api/', include('api.urls')),
]
api/urls.py
from django.urls import path
from .views import UserView
urlpatterns = [
path('users/', UserView.as_view()),
]
views.py
from django.http import HttpResponse
from rest_framework.views import APIView
from rest_framework.versioning import QueryParameterVersioning
class UserView(APIView):
versioning_class = QueryParameterVersioning
def get(self, request, *args, **kwargs):
print(request.version)
return HttpResponse('User List')
settings.py
REST_FRAMEWORK = {
"DEFAULT_VERSION": 'v1',
"ALLOWED_VERSIONS": ['v1', 'v2'],
"VERSION_PARAM": 'version',
}
Query Parameter Versioning
QueryParameterVersioning extracts the version from query string parameters.
Request:
http://127.0.0.1:8000/api/users/?version=v2
The request.version attribute contains the current version. If no version is provided, it defaults to DEFAULT_VERSION ('v1').
Request:
http://127.0.0.1:8000/api/users/
If a version outside the allowed range is requested, an error is raised.
Request:
http://127.0.0.1:8000/api/users/?version=v3
URL Path Versioning
Configuration
Modify api/urls.py:
from django.urls import path, re_path
from .views import UserView
urlpatterns = [
re_path(r'^(?P<version>[v1|v2]+)/users/$', UserView.as_view()),
]
Modify views.py:
from rest_framework.versioning import URLPathVersioning
class UserView(APIView):
versioning_class = URLPathVersioning
def get(self, request, *args, **kwargs):
print(request.version)
return HttpResponse('User List')
Request:
http://127.0.0.1:8000/api/v2/users/
Global Configuration
To avoid repeating versioning configuration in every view, configure it globally in settings.py:
REST_FRAMEWORK = {
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
}
Then simplify views.py:
class UserView(APIView):
def get(self, request, *args, **kwargs):
print(request.version)
return HttpResponse('User List')
Reverse URL Resolution
Add a name to the URL pattern:
urlpatterns = [
re_path(r'^(?P<version>[v1|v2]+)/users/$', views.UserView.as_view(), name='api_user'),
]
Use reverse resolution in the view:
class UserView(APIView):
def get(self, request, *args, **kwargs):
print(request.version)
print(request.versioning_scheme)
resolved_url = request.versioning_scheme.reverse(viewname='api_user', request=request)
print(resolved_url)
return HttpResponse('User List')
Request:
http://127.0.0.1:8000/api/v1/users/
Source Code Analysis
Entry Point: dispatch
def dispatch(self, request, *args, **kwargs):
self.args = args
self.kwargs = kwargs
request = self.initialize_request(request, *args, **kwargs)
self.request = request
self.headers = self.default_response_headers
try:
self.initial(request, *args, **kwargs)
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
response = handler(request, *args, **kwargs)
except Exception as exc:
response = self.handle_exception(exc)
self.response = self.finalize_response(request, response, *args, **kwargs)
return self.response
Initialization: initial
def initial(self, request, *args, **kwargs):
self.format_kwarg = self.get_format_suffix(**kwargs)
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme
self.perform_authentication(request)
self.check_permissions(request)
self.check_throttles(request)
Version Determination: determine_version
def determine_version(self, request, *args, **kwargs):
if self.versioning_class is None:
return (None, None)
scheme = self.versioning_class()
return (scheme.determine_version(request, *args, **kwargs), scheme)
The method first checks if versioning_class is defined in the view. If not, it falls back to the global configuration. Both are class references that get instatniated.
QueryParameterVersioning Implementation
def determine_version(self, request, *args, **kwargs):
version = request.query_params.get(self.version_param, self.default_version)
if not self.is_allowed_version(version):
raise exceptions.NotFound(self.invalid_version_message)
return version
URLPathVersioning Implementation
class URLPathVersioning(BaseVersioning):
invalid_version_message = _('Invalid version in URL path.')
def determine_version(self, request, *args, **kwargs):
version = kwargs.get(self.version_param, self.default_version)
if not self.is_allowed_version(version):
raise exceptions.NotFound(self.invalid_version_message)
return version
BaseVersioning Class
class BaseVersioning(object):
default_version = api_settings.DEFAULT_VERSION
allowed_versions = api_settings.ALLOWED_VERSIONS
version_param = api_settings.VERSION_PARAM
def determine_version(self, request, *args, **kwargs):
msg = '{cls}.determine_version() must be implemented.'
raise NotImplementedError(msg.format(cls=self.__class__.__name__))
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
return _reverse(viewname, args, kwargs, request, format, **extra)
def is_allowed_version(self, version):
if not self.allowed_versions:
return True
return ((version is not None and version == self.default_version) or
(version in self.allowed_versions))
The three configurable class attributes map to these settings:
default_version→DEFAULT_VERSIONallowed_versions→ALLOWED_VERSIONSversion_param→VERSION_PARAM
The final result gets attached to the request object:
request.version, request.versioning_scheme = version, scheme
This allows access to both the version string and the versioning scheme instance for reverse URL resolution and other operations.