Skip to content

Versioning

API versioning is the practice of managing changes to an API over time by explicitly distinguishing between different iterations of its endpoints.

As your API evolves, you will inevitably need to introduce breaking changes - such as renaming a field, modifying a response structure, or changing underlying logic. Versioning allows you to maintain backward compatibility for existing clients (like older mobile app versions) while simultaneously offering newer, improved behavior for new clients.

Default Behavior

By default, DRF's DEFAULT_VERSIONING_CLASS setting is None. This means that no versioning is applied automatically and the request.version attribute will always be None.

Configuring the Versioning Scheme

The most common approach is to configure the versioning style globally in your settings.py.

python
REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning',
    'DEFAULT_VERSION': 'v1',          # Default version when none is provided
    'ALLOWED_VERSIONS': ['v1', 'v2'], # Optional list of valid versions
    'VERSION_PARAM': 'version'        # String to use for versioning parameters
}

NOTE

The DEFAULT_VERSION is always considered valid and allowed by DRF, even if it is missing from the ALLOWED_VERSIONS list.

You can also override the global versioning class on an individual view by setting the versioning_class attribute.

python
from rest_framework.versioning import URLPathVersioning
from rest_framework.views import APIView

class ProfileList(APIView):
    versioning_class = URLPathVersioning

Accessing the Version

Once a versioning scheme is configured, the determined version string is attached to the request object and can be accessed as request.version within your view logic.

Built-in Versioning Schemes

DRF provides several built-in versioning classes to support different strategies.

AcceptHeaderVersioning

The AcceptHeaderVersioning scheme expects the client to specify the version as part of the media type in the Accept header. This is often considered the most "RESTful" approach, as it uses standard HTTP content negotiation.

Request:

http
GET /bookings/ HTTP/1.1
Host: example.com
Accept: application/json; version=1.0

On the server, you can then branch your logic based on the request.version attribute to handle the request accordingly. A common pattern is to dynamically select a different serializer for newer versions:

python
from rest_framework import viewsets
from .models import Booking
from .serializers import OldBookingSerializer, BookingSerializer

class BookingViewSet(viewsets.ModelViewSet):
    queryset = Booking.objects.all()

    def get_serializer_class(self):
        if self.request.version == '1.0':
            return OldBookingSerializer
        return BookingSerializer

Caching and AcceptHeaderVersioning

Because the URL remains identical across versions, a cached v1 response will be served to a v2 client unless you vary on Accept header. Django's @cache_page does not vary on the Accept header by default (but only Cookie and Accept-Language/Accept-Encoding).

Any downstream CDN or reverse proxy will also have this problem and requires the Vary: Accept response header to behave correctly.

python
from django.utils.decorators import method_decorator
from django.views.decorators.vary import vary_on_headers

class BookingViewSet(viewsets.ModelViewSet):
    # ...

    @method_decorator(vary_on_headers("Accept"))
    def dispatch(self, request, *args, **kwargs):
        return super().dispatch(request, *args, **kwargs)

URLPathVersioning

The URLPathVersioning scheme requires the version to be specified explicitly as part of the URL path. This is a very common and highly visible pattern.

Request:

http
GET /v1/bookings/ HTTP/1.1
Host: example.com
Accept: application/json

Your URL conf must include a pattern that matches the version with a version keyword argument:

python
from django.urls import re_path
from .views import BookingsList

urlpatterns = [
    re_path(r'^(?P<version>(v1|v2))/bookings/$', BookingsList.as_view(), name='bookings-list'),
]

NamespaceVersioning

To the client, the NamespaceVersioning scheme looks exactly the same as URLPathVersioning. However, on the server side, it uses Django's URL namespacing, instead of URL keyword arguments, to determine the requested version.

python
# urls.py
from django.urls import path, include

urlpatterns = [
    path('v1/', include('bookings.urls', namespace='v1')),
    path('v2/', include('bookings.urls', namespace='v2')),
]

HostNameVersioning

The HostNameVersioning scheme requires the client to specify the requested version as a subdomain in the hostname.

Request:

http
GET /bookings/ HTTP/1.1
Host: v1.example.com
Accept: application/json

QueryParameterVersioning

The QueryParameterVersioning scheme specifies the version as a simple query parameter appended to the URL.

Request:

http
GET /something/?version=0.1 HTTP/1.1
Host: example.com
Accept: application/json

Versioned APIs and Hyperlinked Serializers

When using hyperlinked serialization alongside a versioning scheme, ensure that you pass the request as context to the serializer. DRF will automatically apply the current version to any hyperlinked URLs generated by the serializer so that links point to the correct API version.

python
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Booking
from .serializers import BookingSerializer

class BookingListView(APIView):
    def get(self, request):
        queryset = Booking.objects.all()
        # Passing the request in the context is required for versioned hyperlinked URLs
        serializer = BookingSerializer(queryset, many=True, context={'request': request})
        return Response(serializer.data)

Custom Versioning Schemes

If the built-in schemes do not fit your requirements, you can implement a custom versioning scheme by subclassing BaseVersioning and overriding the determine_version method.

python
from rest_framework.versioning import BaseVersioning

class CustomHeaderVersioning(BaseVersioning):
    def determine_version(self, request, *args, **kwargs):
        # Extract the version from a custom HTTP header
        return request.META.get('HTTP_X_API_VERSION', self.default_version)

You can then apply this custom scheme globally in your settings or on a per-view basis, just like the built-in classes.

Reversing URLs

When using versioning, you should always use DRF's reverse function to construct URLs within your API. It will automatically detect the active request.version and incorporate it into the generated URL correctly.

python
from rest_framework.reverse import reverse
from rest_framework.views import APIView
from rest_framework.response import Response

class BookingInfo(APIView):
    def get(self, request):
        # Automatically includes the correct version parameter/path/namespace
        url = reverse('bookings-list', request=request)
        return Response({'bookings_url': url})

TIP

If you are building a new API and are unsure which scheme to pick, consider starting with URLPathVersioning or NamespaceVersioning. They are explicit, easy to test in a browser and widely understood by developers integrating with your endpoints.