Django REST Framework Versioning Implementation Deep Dive

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_VERSION
  • allowed_versions → ALLOWED_VERSIONS
  • version_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.

Tags: Django REST Framework API Versioning source code analysis URL routing python

Posted on Tue, 13 Oct 2026 16:28:26 +0000 by danieliser