DRF

Django Rest Framework

DRF

Makes it easy to:

  • serialize objects to JSON

  • deseralize from JSON to objects

Class-based Views

DRF provides an APIView class, which subclasses Django's View class.

APIView vs regular View classes:

  • Requests passed to the handler methods will be REST framework's Request instances, not Django's HttpRequestinstances.

  • Handler methods may return REST framework's Response, instead of Django's HttpResponse. The view will manage content negotiation and setting the correct renderer on the response.

  • Any APIException exceptions will be caught and mediated into appropriate responses.

  • Incoming requests will be authenticated and appropriate permission and/or throttle checks will be run before dispatching the request to the handler method.

Using the APIView class is pretty much the same as using a regular View class, as usual, the incoming request is dispatched to an appropriate handler method such as .get() or .post(). Additionally, a number of attributes may be set on the class that control various aspects of the API policy.

Example

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication, permissions
from django.contrib.auth.models import User

class ListUsers(APIView):
    """
    View to list all users in the system.

    * Requires token authentication.
    * Only admin users are able to access this view.
    """
    authentication_classes = (authentication.TokenAuthentication,)
    permission_classes = (permissions.IsAdminUser,)

    def get(self, request, format=None):
        """
        Return a list of all users.
        """
        usernames = [user.username for user in User.objects.all()]
        return Response(usernames)

Function Based Views

DRF also allows you to work with regular function based views. It provides a set of simple decorators that wrap your function based views to ensure they receive an instance of Request (rather than the usual Django HttpRequest) and allows them to return a Response (instead of a Django HttpResponse), and allow you to configure how the request is processed.

Signature: @api_view(http_method_names=['GET'])

Decorator, which takes a list of HTTP methods that your view should respond to.

Example: simple view that just manually returns some data:

from rest_framework.decorators import api_view

@api_view()
def hello_world(request):
    return Response({"message": "Hello, world!"})

This view will use the default renderers, parsers, authentication classes etc specified in the settings.

By default only GET methods will be accepted. Other methods will respond with "405 Method Not Allowed". To alter this behaviour, specify which methods the view allows, like so:

@api_view(['GET', 'POST'])
def hello_world(request):
    if request.method == 'POST':
        return Response({"message": "Got some data!", "data": request.data})
    return Response({"message": "Hello, world!"})

Viewsets

DRF allows you to combine the logic for a set of related views in a single class, called a ViewSet. In other frameworks called 'Resources' or 'Controllers'.

A type of class-based View, that does not provide any method handlers such as .get() or .post(), and instead provides actions such as .list() and .create().

The method handlers for a ViewSet are only bound to the corresponding actions at the point of finalizing the view, using the .as_view() method.

Typically, rather than explicitly registering the views in a viewset in the urlconf, you'll register the viewset with a router class, that automatically determines the urlconf for you.

Let's define a simple viewset that can be used to list or retrieve all the users in the system.

from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from myapps.serializers import UserSerializer
from rest_framework import viewsets
from rest_framework.response import Response

class UserViewSet(viewsets.ViewSet):
    """
    A simple ViewSet for listing or retrieving users.
    """
    def list(self, request):
        queryset = User.objects.all()
        serializer = UserSerializer(queryset, many=True)
        return Response(serializer.data)

    def retrieve(self, request, pk=None):
        queryset = User.objects.all()
        user = get_object_or_404(queryset, pk=pk)
        serializer = UserSerializer(user)
        return Response(serializer.data)

If we need to, we can bind this viewset into two separate views, like so:

user_list = UserViewSet.as_view({'get': 'list'})
user_detail = UserViewSet.as_view({'get': 'retrieve'})

Typically we wouldn't do this, but would instead register the viewset with a router, and allow the urlconf to be automatically generated.

from myapp.views import UserViewSet
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'users', UserViewSet, basename='user')
urlpatterns = router.urls

Rather than writing your own viewsets, you'll often want to use the existing base classes that provide a default set of behavior. For example:

class UserViewSet(viewsets.ModelViewSet):
    """
    A viewset for viewing and editing user instances.
    """
    serializer_class = UserSerializer
    queryset = User.objects.all()

2 advantages of using a ViewSet class over using a View class.

  • Repeated logic can be combined into a single class. In the above example, we only need to specify the queryset once, and it'll be used across multiple views.

  • By using routers, we no longer need to deal with wiring up the URL conf ourselves.

Both of these come with a trade-off. Using regular views and URL confs is more explicit and gives you more control. ViewSets are helpful if you want to get up and running quickly, or when you have a large API and you want to enforce a consistent URL configuration throughout.

The default routers included with REST framework will provide routes for a standard set of create/retrieve/update/destroy style actions, as shown below:

class UserViewSet(viewsets.ViewSet):
    """
    Example empty viewset demonstrating the standard
    actions that will be handled by a router class.

    If you're using format suffixes, make sure to also include
    the `format=None` keyword argument for each action.
    """

    def list(self, request):
        pass

    def create(self, request):
        pass

    def retrieve(self, request, pk=None):
        pass

    def update(self, request, pk=None):
        pass

    def partial_update(self, request, pk=None):
        pass

    def destroy(self, request, pk=None):
        pass

During dispatch, the following attributes are available on the ViewSet.

  • basename - the base to use for the URL names that are created.

  • action - the name of the current action (e.g., list, create).

  • detail - boolean indicating if the current action is configured for a list or detail view.

  • suffix - the display suffix for the viewset type - mirrors the detail attribute.

  • name - the display name for the viewset. This argument is mutually exclusive to suffix.

  • description - the display description for the individual view of a viewset.

You may inspect these attributes to adjust behaviour based on the current action. Example, restrict permissions to everything except the list:

def get_permissions(self):
    """
    Instantiates and returns the list of permissions that this view requires.
    """
    if self.action == 'list':
        permission_classes = [IsAuthenticated]
    else:
        permission_classes = [IsAdmin]
    return [permission() for permission in permission_classes]

Serializers

Definitinon

Allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON, XML or other content types.

Also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.

Declaration

from rest_framework import serializers

class CommentSerializer(serializers.Serializer):
    email = serializers.EmailField()
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

Serialization

serializer = CommentSerializer(comment)
serializer.data
# {'email': 'leila@example.com', 'content': 'foo bar', 'created': '2016-01-27T15:17:10.375877'}

At this point we've translated the model instance into Python native datatypes. To finalise the serialization process we render the data into json.

from rest_framework.renderers import JSONRenderer

json = JSONRenderer().render(serializer.data)
json
# b'{"email":"leila@example.com","content":"foo bar","created":"2016-01-27T15:17:10.375877"}'

Deserializing objects

1st. Parse a stream into Python native datatypes...

import io
from rest_framework.parsers import JSONParser

stream = io.BytesIO(json)
data = JSONParser().parse(stream)

2nd. restore those native datatypes into a dictionary of validated data.

serializer = CommentSerializer(data=data)
serializer.is_valid()
# True
serializer.validated_data
# {'content': 'foo bar', 'email': 'leila@example.com', 'created': datetime.datetime(2012, 08, 22, 16, 20, 09, 822243)}

Validation. When deserializing objects, you always need to call is_valid() before attempting to access the validated data, or save an object instance:

serializer = CommentSerializer(data={'email': 'foobar', 'content': 'baz'})
serializer.is_valid()
# False
serializer.errors
# {'email': ['Enter a valid e-mail address.'], 'created': ['This field is required.']}

Dealing with nested objects

represent more complex objects, where some of the attributes of an object might not be simple datatypes such as strings, dates or integers.

The Serializer class is itself a type of Field, and can be used to represent relationships where one object type is nested inside another.

class UserSerializer(serializers.Serializer):
    email = serializers.EmailField()
    username = serializers.CharField(max_length=100)

class CommentSerializer(serializers.Serializer):
    user = UserSerializer()
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

If a nested representation may optionally accept the None value you should pass the required=False flag to the nested serializer.

class CommentSerializer(serializers.Serializer):
    user = UserSerializer(required=False)  # May be an anonymous user.
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

Similarly if a nested representation should be a list of items, you should pass the many=True flag to the nested serialized.

class CommentSerializer(serializers.Serializer):
    user = UserSerializer(required=False)
    edits = EditItemSerializer(many=True)  # A nested list of 'edit' items.
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

When dealing with nested representations that support deserializing the data, any errors with nested objects will be nested under the field name of the nested object.

serializer = CommentSerializer(data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'})
serializer.is_valid()
# False
serializer.errors
# {'user': {'email': ['Enter a valid e-mail address.']}, 'created': ['This field is required.']}

Similarly, the .validated_data property will include nested data structures.

Writing .create() methods for nested representations

If you're supporting writable nested representations you'll need to write .create() or .update() methods that handle saving multiple objects.

Example: handling creating a user with a nested profile object:

class UserSerializer(serializers.ModelSerializer):
    profile = ProfileSerializer()

    class Meta:
        model = User
        fields = ('username', 'email', 'profile')

    def create(self, validated_data):
        profile_data = validated_data.pop('profile')
        user = User.objects.create(**validated_data)
        Profile.objects.create(user=user, **profile_data)
        return user

Writing .update() methods for nested representations

For updates you'll want to think carefully about how to handle updates to relationships. For example if the data for the relationship is None, or not provided, which of the following should occur?

  • Set the relationship to NULL in the database.

  • Delete the associated instance.

  • Ignore the data and leave the instance as it is.

  • Raise a validation error.

Example: an .update() method on our previous UserSerializer class.

    def update(self, instance, validated_data):
        profile_data = validated_data.pop('profile')
        # Unless the application properly enforces that this field is
        # always set, the follow could raise a `DoesNotExist`, which
        # would need to be handled.
        profile = instance.profile

        instance.username = validated_data.get('username', instance.username)
        instance.email = validated_data.get('email', instance.email)
        instance.save()

        profile.is_premium_member = profile_data.get(
            'is_premium_member',
            profile.is_premium_member
        )
        profile.has_support_contract = profile_data.get(
            'has_support_contract',
            profile.has_support_contract
         )
        profile.save()

        return instance

Because the behavior of nested creates and updates can be ambiguous, and may require complex dependencies between related models, REST framework 3 requires you to always write these methods explicitly. The default ModelSerializer .create() and .update() methods do not include support for writable nested representations.

There are however, third-party packages available such as DRF Writable Nested that support automatic writable nested representations.

Handling saving related instances in model manager classes

An alternative to saving multiple related instances in the serializer is to write custom model manager classes that handle creating the correct instances.

For example, suppose we wanted to ensure that User instances and Profile instances are always created together as a pair. We might write a custom manager class that looks something like this:

class UserManager(models.Manager):
    ...

    def create(self, username, email, is_premium_member=False, has_support_contract=False):
        user = User(username=username, email=email)
        user.save()
        profile = Profile(
            user=user,
            is_premium_member=is_premium_member,
            has_support_contract=has_support_contract
        )
        profile.save()
        return user

This manager class now more nicely encapsulates that user instances and profile instances are always created at the same time. Our .create() method on the serializer class can now be re-written to use the new manager method.

def create(self, validated_data):
    return User.objects.create(
        username=validated_data['username'],
        email=validated_data['email']
        is_premium_member=validated_data['profile']['is_premium_member']
        has_support_contract=validated_data['profile']['has_support_contract']
    )

For more details on this approach see the Django documentation on model managers, and this blogpost on using model and manager classes.

API Views

Django views that will use the previously created class to class to return JSON representation for each HTTP request that our API will handle.

Authentication

Throttling

DRF provedes 3 throttling classes:

  1. AnonRateThrottle: limits the rate of request that an anonymous user can make requests

  2. UserRateThrotle: limits the rate of request that a specific user can make requests

  3. ScopedRateThrottle: limits the rate of requests for specific parts of the API identified

Last updated