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.
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.
from rest_framework.versioning import URLPathVersioning
from rest_framework.views import APIView
class ProfileList(APIView):
versioning_class = URLPathVersioningAccessing 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:
GET /bookings/ HTTP/1.1
Host: example.com
Accept: application/json; version=1.0On 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:
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 BookingSerializerCaching 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.
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:
GET /v1/bookings/ HTTP/1.1
Host: example.com
Accept: application/jsonYour URL conf must include a pattern that matches the version with a version keyword argument:
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.
# 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:
GET /bookings/ HTTP/1.1
Host: v1.example.com
Accept: application/jsonQueryParameterVersioning
The QueryParameterVersioning scheme specifies the version as a simple query parameter appended to the URL.
Request:
GET /something/?version=0.1 HTTP/1.1
Host: example.com
Accept: application/jsonVersioned 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.
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.
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.
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.
