Back
Mar 2, 2017

API versioning with django rest framework?

What the versioning is used for?

We often handling API server updates including backwards-incompatible changes when upgrading web applications. At the same time we update the client part, therefore, we did not experience any particular difficulties.

Though, in the case of handling API updates for a mobile app, the process of upgrading applications in the user's phone does not occur simultaneously. Moreover, users may have various versions of the application, and we have to keep all API versions up and running.

An app should have a nice versioned API: changes and new functionalities have to be implemented in the new API versions, not just in one same version. Existing clients can continue to use the old compatible API version with the new one. New or upgraded clients can use a new version.

Versioning schemes supported in drf

DRF supports multiple versioning schemes:

AcceptHeaderVersioning – transfer of the version number through the Accept request header:

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

URLPathVersioning – adding the version to a resource address of a variable (the path is specified in the DRF through the VERSION_PARAM parameter):

urlpatterns = [
    url(
        r'^(?P(v1|v2))/bookings/$',
        bookings_list,
        name='bookings-list'
    )
]

NamespaceVersioning – the version is given through the url namespace:

# urls.py
urlpatterns = [
    url(r'^v1/bookings/', include('bookings.urls', namespace='v1')),
    url(r'^v2/bookings/', include('bookings.urls', namespace='v2'))
]

HostNameVersioning – the version is set by the domain name:

http://v1.example.com/bookings/
http://v2.example.com/bookings/

QueryParameterVersioning – transfer of the version through the GET parameter:

http://example.com/bookings/?version=0.1
http://example.com/bookings/?version=0.2

Versioning the code

The first versioning method is described in the Django REST Framework documentation.

Creating Serializer and ViewSet:

class AccountSerializer(serializers.ModelSerializer):
    class Meta:
        model = Account
        fields = ('id', 'account_name', 'users', 'created')


class AccountViewSet(viewsets.ModelViewSet):
    queryset = Account.objects.all()
    serializer_class = AccountSerializer

If we need to change/delete/add a field, we create a new serializer and change fields in it.

class AccountSerializerVersion1(serializers.ModelSerializer):
    class Meta:
        model = Account
        fields = ('id', 'account_name', 'users', 'created', 'updated')

And then we redefine the get_serializer_class method in AccountViewSet:

def get_serializer_class(self):
    if self.request.version == 'v1':
        return AccountSerializerVersion1
    return AccountSerializer

That’s one way to redefine the serializer, permission class and other methods in ViewSet.

As well, I found a small application for versioning.

I did not use it, though, we got from the description that we can set the Serializer and Parser and use them to set the transform’s base class.

from rest_framework_transforms.transforms import BaseTransform


class TestModelTransform0002(BaseTransform):
    """
    Changes between v1 and v2
    """
    def forwards(self, data, request):
        if 'test_field_one' in data:
            data['new_test_field'] = data.get('test_field_one')
            data.pop('test_field_one')
        return data

    def backwards(self, data, request, instance):
        data['test_field_one'] = data.get('new_test_field')
        data.pop('new_test_field')
        return data

Setting the basic version:

class TestSerializerV3(BaseVersioningSerializer):
    transform_base = 'tests.test_transforms.TestModelTransform'

    class Meta:
        model = TestModelV3
        fields = (
            'test_field_two',
            'test_field_three',
            'test_field_four',
            'test_field_five',
            'new_test_field',
            'new_related_object_id_list',
        )

And we do so for every new version:

class TestModelTransform0003(BaseTransform):
    """
    Changes between v2 and v3
    """

    def forwards(self, data, request):
        data['new_related_object_id_list'] = [1, 2, 3, 4, 5]
        return data

    def backwards(self, data, request, instance):
        data.pop('new_related_object_id_list')
        return data

The backwards methods would apply from the end to the beginning when receiving data from client, i.e. 0004, 0003, 0002. When sending data to the client, the forwards would apply in direct order 0002, 0003, 0004.

How we handled versioning

The basic idea was to split the API into the modules and use the class inheritance.

The following directory structure is:

api/
├── base
│   ├── init.py
│   ├── router.py
│   ├── serializers.py
│   └── views.py
├── init.py
└── versioned
    ├── init.py
    ├── v2
    │   ├── init.py
    │   ├── router.py
    │   ├── serializers.py
    │   └── views.py
    ├── v3
    │   ├── init.py
    │   ├── router.py
    │   ├── serializers.py
    │   └── views.py
    ├── v4
    │   ├── init.py
    │   ├── router.py
    │   ├── serializers.py
    │   └── views.py
    └── v5
        ├── init.py
        ├── router.py
        ├── serializers.py
        └── views.py

base - the first version of our API.

Further, in the versioned folder, we created a folder for each version. In this project we had two external clients: iOS and Android + our WEB client. The WEB client has always used the latest version of the API.

Each successive API version was prepared this way: we made changes in the existing API v2; after the iOS and Android client release (they released them at the same time), we’ve created v3 and stopped making changes to v2. We could change Version 3 before the next release of the external clients.

DRF uses classes to create ViewSet, Serializer, Permission. We used inheritance between the API versions to not fully copy ViewSets and Serializers.

# base/serializers.py

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'first_name', 'last_name', 'email')


class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = 'all'
# base/views.py
from . import serializers


class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = serializers.UserSerializer


class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = serializers.BookSerializer
# base/router.py
from . import views


router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'books', views.BookViewSet)

api_urlpatterns = router.urls

Further, we generally connected urls.py to the first API version:

from .api.base.router import api_urlpatterns as api_v1


urlpatterns = [
    url(r'^api/v1/', include(api_v1)),
]

We removed the first_name, last_name fields and added the full_name fields. Then we created v2 keeping the backward compatibility and addde the serializers.py, views.py, router.py directories and files:

└── versioned
    ├── init.py
    ├── v2
    │   ├── init.py
    │   ├── router.py
    │   ├── serializers.py
    │   └── views.py

inheriting the basic version:

# versioned/v2/serializers.py

# import all our basic serializers

from .api.base import serializers as base_serializers
from .api.base.serializers import *


class UserSerializer(base_serializers.UserSerializer):
    full_name = serializers.SerializerMethodField()

    class Meta(base_serializers.UserSerializer.Meta):
        fields = ('id', 'email', 'full_name')

    def get_full_name(self, obj):
        return '{0} {1}'.format(obj.first_name, obj.last_name)
# versioned/v2/views.py
from .api.base.views import *
from .api.base import views as base_views
from . import serializers as v2_serializers


class UserViewSet(base_views.UserViewSet):
    serializer_class = v2_serializers.UserSerializer
# versioned/v2/router.py
from . import views


router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'books', views.BookViewSet)

api_urlpatterns = router.urls

Updating the urls.py root:

from .api.base.router import api_urlpatterns as api_v1
from .api.versioned.v2.router import api_urlpatterns as api_v2


urlpatterns = [
    url(r'^api/v1/', include(api_v1)),
    url(r'^api/v2/', include(api_v2)),
]

You may notice that we’ve inherited UserViewSet, and we did not have to update BookViewSet, that’s why we used the first version of the view.

Pros and cons of the approach

Pros:

  • Simple implementation
  • Versions of relevant classes are classified by the module base, v1, v2, etc.
  • Easy to navigate the code
  • No need to copy the view source code and serializers
  • Less IF nesting

Cons:

  • Deep level of nesting occurs when there are a large number of API versions
  • Slightly harder to debug code due to nest

Read Also: SaaS Development Trends

CONCLUSION

It can be quite difficult to manage the API versions, especially if you want to do it properly. You can find pros and cons in each versioning method. And the inheritance method proved to be good due to small number of versions in our project.

Subscribe for the news and updates

More thoughts
Jul 27, 2022Technology
Forge Viewer: Our Experience with an Unusual Project

Once we received an interesting task from a client. They needed to allow their users to upload a 3D model of the building and show it in a timelapse video from the construction site.

Jul 13, 2022Technology
Prosemirror: Render node as react component

In this article I’m going to show how to declare custom prosemirror node, how to render it with toDom method and how improve that with custom NodeView using React component.

Apr 27, 2022TechnologyBusiness
How to Choose the Best Javascript Framework: Comparison of the Top Javascript Frameworks

In our article, you will find the best JavaScript framework comparison so that you know for sure how to choose the right one for your project.

Mar 12, 2017Technology
Creating a chat with Django Channels

Nowadays, when every second large company has developed its own instant messenger, in the era of iMessages, Slack, Hipchat, Messager, Google Allo, Zulip and others, I will tell you how to keep up with the trend and write your own chat, using django-channels 0.17.3, django 1.10.x, python 3.5.x.

Feb 28, 2017Technology
Passing pieces of markup to components in Angular 2 and problems with dynamic content

In this article we'll research how to pass custom markup to Angular components and how to create different types of dynamic components.

Aug 31, 2016Technology
Angular vs React Comparison

In this article, we will compare two most popular JS Libraries (Angular vs React). Both of them were created by professionals and have been used in famous big projects.