Django Rest Framework

  • Why use web APIs?
  • Why not just using Django to create APIs?
  • What is Django Rest Framework

Model

from django.db import models


class Meal(models.Model):
    owner = models.ForeignKey('auth.User', related_name='meals')
    name = models.CharField(max_length=255)
    notes = models.TextField(blank=True)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

    class Meta:
        ordering = ('name',)

Serializer

from rest_framework import serializers
from .models import Meal


class MealSerializer(serializers.HyperlinkedModelSerializer):
    owner = serializers.Field(source='owner.username')

    class Meta:
        model = Meal
        fields = ('url', 'owner', 'name', 'notes')
  • Serializer 
  • ModelSerializer
  • HyperlinkedModelSerializer
  • Third party packages (MongoEngineModelSerializer GeoFeatureModelSerializer, HStoreSerializer)

Function Based Views 

from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response

from .models import Meal
from .serializers import MealSerializer


@api_view(['GET', 'POST'])
def meal_list(request):
    if request.method == 'GET':
        meal = Meal.objects.all()
        serializer = MealSerializer(meal, many=True)
        return Response(serializer.data)

    elif request.method == 'POST':
        serializer = MealSerializer(data=request.DATA)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Beatiful HTTP Status codes

Any attempt to brew coffee with a teapot should result in the error code "418 I'm a teapot". The resulting entity body MAY be short and stout.

                                                           — RFC 2324, Hyper Text Coffee Pot Control Protocol

  • Informational - 1xx (eg. HTTP_100_CONTINUE)
  • Successful - 2xx (eg. HTTP_200_OK, HTTP_201_CREATED, HTTP_202_ACCEPTED)
  • Redirection - 3xx (eg. HTTP_301_MOVED_PERMANENTLY)
  • Client Error - 4xx (eg. HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN)
  • Server Error - 5xx (eg. HTTP_500_INTERNAL_SERVER_ERROR)

418

I'm a teapot

Class Based Views

from rest_framework import status
from rest_framework.response import Response

from rest_framework.views import APIView

from .models import Meal
from .serializers import MealSerializer


class MealList(APIView):
    def get(self, request, format=None):
        meal = Meal.objects.all()
        serializer = MealSerializer(meal, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        serializer = MealSerializer(data=request.DATA)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Mixins

from rest_framework import mixins
from rest_framework import generics

from .models import Meal
from .serializers import MealSerializer


class MealList(mixins.ListModelMixin,
               mixins.CreateModelMixin,
               generics.GenericAPIView):
    queryset = Meal.objects.all()
    serializer_class = MealSerializer

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

Generic Class Based Views

from rest_framework import generics

from .models import Meal
from .serializers import MealSerializer


class MealList(generics.ListCreateAPIView):
    queryset = Meal.objects.all()
    serializer_class = MealSerializer

But as programmers, we always ask ourselves the same question over and over again.

Very Powerful huh?

Can we do better?

Viewsets

from rest_framework import viewsets

from .models import Meal
from .serializers import MealSerializer


class MealViewSet(viewsets.ModelViewSet):
    queryset = Meal.objects.all()
    serializer_class = MealSerializer

Routers

from rest_framework.routers import DefaultRouter

from .views import MealViewSet

router = DefaultRouter()
router.register(r'meal', MealViewSet)

urlpatterns = [
    url(r'^', include(router.urls)),
]

Permissions

from rest_framework import permissions, viewsets

from .permissions import IsOwner


class MealViewSet(viewsets.ModelViewSet):
    queryset = Meal.objects.all()
    serializer_class = MealSerializer
    permission_classes = (permissions.IsAuthenticated, IsOwner,)

    def pre_save(self, obj):
        obj.owner = self.request.user

Permissions

from rest_framework import permissions


class IsOwner(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object to view 
    and edit it.
    """

    def has_object_permission(self, request, view, obj):
        return obj.owner == request.user

Permissions


REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}
  • pre_save method
  • post_save method
  • Fitlers
  • pagination
  • @detail_route

Other useful stuff

Authentication

  • BasicAuthentication

  • TokenAuthentication

  • SessionAuthentication

  • OAuthAuthentication

  • OAuth2Authentication

  • Digest Authentication

  • Django OAuth Toolkit

  • Django OAuth2 Consumer

  • JSON Web Token Authentication

  • Hawk HTTP Authentication

  • HTTP Signature Authentication

Testablity

Code without tests is broken as designed.

— Jacob Kaplan-Moss

from django.contrib.auth.models import User
from django.core.urlresolvers import reverse

from rest_framework import status
from rest_framework.test import APITestCase

from .models import Meal


class MealViewTest(APITestCase):

    def test_add_meal_will_save_the_owner(self):
        user_name, user_password, user = create_user()
        user_auth(self.client, user_name, user_password)
        data = {'name': 'new meal'}
        response = self.client.post(
            reverse('meal-list'), data, format='json'
        )

        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.data['owner'], 'john')


def create_user():
    user_name = 'john'
    user_email = 'lennon@thebeatles.com'
    user_password = 'johnpassword'
    user = User.objects.create_user(user_name, user_email, user_password)

    return user_name, user_password, user


def user_auth(api_client, user_name, user_password):
    api_client.login(username=user_name, password=user_password)

Working with AJAX, CSRF & CORS

https://docs.djangoproject.com/en/dev/ref/csrf/#ajax

https://github.com/ottoyiu/django-cors-headers


RestangularProvider.setDefaultHeaders({"X-CSRFToken": csrftoken});

$http.defaults.headers.common["X-CSRFToken"] = csrftoken;

What do you think?

  • Very similar APIs to Django 
  • Flexible views
  • Browseable APIs
  • Supports many Authentication policies
  • Security
  • Testability
  • Extensive documentation and great community support.
  • Used and trusted by large companies such as Mozilla, Eventbrite and many more (see kickstarter campaign)
  • 3.0 is coming

Summary

Questions?

Made with Slides.com