$30 off During Our Annual Pro Sale. View Details »

What You Should Know About Django REST Framework

What You Should Know About Django REST Framework

When I started writing Django REST Framework code in 2017, there was a lot I didn't know, like how class-based views worked and what a serializer was.

After 3+ years of using DRF, I want to share the things I've learned that make writing DRF code easier and faster for me. You will learn how to save time and lines of code by using DRF's built-in viewsets (and what a viewset is), when to skip the viewset and use a built-in generic `APIView`, and how to add custom endpoints (actions) to your viewsets. You'll also learn how modular DRF can be when you customize built-in methods or use different serializers for different parts of your viewset, and how tools like Classy DRF can help.

You will walk away from this talk ready to start your first DRF project, make your existing projects simpler and cleaner, or (at the very least) with a better understanding of how a class-based view works.

You will get the most out of this talk if you have beginner-level experience in Python and Django. A tenuous, vague understanding of how class-based views in Django work would be helpful, but is not necessary. Having completed a Django REST Framework tutorial would also be helpful.

Lacey Williams Henschel

February 20, 2021
Tweet

More Decks by Lacey Williams Henschel

Other Decks in Programming

Transcript

  1. What You Should Know About
    Django REST Framework
    Lacey Williams Henschel
    PyCascades, February 2021
    @laceynwilliams | @revsys

    View Slide

  2. What You Should Know About
    Django REST Framework:
    ViewSets Edition!
    @laceynwilliams | @revsys

    View Slide

  3. ClassyDRF
    @laceynwilliams | @revsys

    View Slide

  4. @laceynwilliams | @revsys

    View Slide

  5. @laceynwilliams | @revsys

    View Slide

  6. DRF has good docs
    DRF Docs on ViewSets
    @laceynwilliams | @revsys

    View Slide

  7. ModelViewSet
    @laceynwilliams | @revsys

    View Slide

  8. ModelViewSet = set of views to:
    ✓ create
    ✓ retrieve
    ✓ update
    ✓ list
    ✓ destroy
    @laceynwilliams | @revsys

    View Slide

  9. class ModelViewSet(mixins.CreateModelMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    mixins.DestroyModelMixin,
    mixins.ListModelMixin,
    GenericViewSet):
    """
    A viewset that provides default `create()`,
    `retrieve()`, `update()`, `partial_update()`,
    `destroy()` and `list()` actions.
    """
    pass
    @laceynwilliams | @revsys

    View Slide

  10. Each
    *ModelMixin
    class has its own methods
    that perform the right ac4ons.
    CreateModelMixin
    has a
    create()
    method,
    instead of a
    post()
    method.
    @laceynwilliams | @revsys

    View Slide

  11. This ViewSet...
    # views.py
    from rest_framework.viewsets import ModelViewSet
    from .models import Book
    from .serializers import BookSerializer
    class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    @laceynwilliams | @revsys

    View Slide

  12. ...gives us these endpoints1
    GET /books/
    GET /books/{id}/
    POST /books/
    PUT /books/{id}/
    PATCH /books/{id}/
    DELETE /books/{id}/
    1 you also need to hook the ViewSet to your urls
    @laceynwilliams | @revsys

    View Slide

  13. A"ributes for your
    ModelViewSet
    @laceynwilliams | @revsys

    View Slide

  14. What objects are you working with?
    from rest_framework.permissions import AllowAny
    from rest_framework.viewsets import ModelViewSet
    from .models import Book
    from .serializers import BookSerializer
    class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    permission_classes = [AllowAny]
    @laceynwilliams | @revsys

    View Slide

  15. How should the data be serialized?
    from rest_framework.permissions import AllowAny
    from rest_framework.viewsets import ModelViewSet
    from .models import Book
    from .serializers import BookSerializer
    class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    permission_classes = [AllowAny]
    @laceynwilliams | @revsys

    View Slide

  16. Who is allowed?
    from rest_framework.permissions import AllowAny
    from rest_framework.viewsets import ModelViewSet
    from .models import Book
    from .serializers import BookSerializer
    class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    permission_classes = [AllowAny]
    @laceynwilliams | @revsys

    View Slide

  17. Methods that come from GenericAPIView
    @laceynwilliams | @revsys

    View Slide

  18. def get_queryset(self):
    assert self.queryset is not None, (
    "'%s' should either include a `queryset` attribute, "
    "or override the `get_queryset()` method."
    % self.__class__.__name__
    )
    queryset = self.queryset
    if isinstance(queryset, QuerySet):
    queryset = queryset.all()
    return queryset
    @laceynwilliams | @revsys

    View Slide

  19. When to override get_queryset():
    When you need to filter your queryset with data
    you don't have un5l the 5me of the request (like
    filter by user)
    @laceynwilliams | @revsys

    View Slide

  20. def get_object(self):
    queryset = self.filter_queryset(self.get_queryset())
    lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
    assert lookup_url_kwarg in self.kwargs, (
    'Expected view %s to be called with a URL keyword argument '
    'named "%s". Fix your URL conf, or set the `.lookup_field` '
    'attribute on the view correctly.' %
    (self.__class__.__name__, lookup_url_kwarg)
    )
    # Uses the lookup_field attribute, which defaults to `pk`
    filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
    obj = get_object_or_404(queryset, **filter_kwargs)
    # May raise a permission denied
    self.check_object_permissions(self.request, obj)
    return obj
    @laceynwilliams | @revsys

    View Slide

  21. In your own methods, you can run:
    obj = self.get_object()
    instead of looking up the object yourself.
    @laceynwilliams | @revsys

    View Slide

  22. def get_serializer_class(self):
    assert self.serializer_class is not None, (
    "'%s' should either include a `serializer_class` attribute, "
    "or override the `get_serializer_class()` method."
    % self.__class__.__name__
    )
    # Returns the attribute you set
    return self.serializer_class
    @laceynwilliams | @revsys

    View Slide

  23. When to override get_serializer_class():
    When you want to use different serializers in
    different situa3ons
    @laceynwilliams | @revsys

    View Slide

  24. def get_serializer(self, *args, **kwargs):
    serializer_class = self.get_serializer_class()
    # The context is where the request is added
    # to the serializer
    kwargs['context'] = self.get_serializer_context()
    return serializer_class(*args, **kwargs)
    @laceynwilliams | @revsys

    View Slide

  25. def get_serializer(self, *args, **kwargs):
    serializer_class = self.get_serializer_class()
    # The context is where the request is added
    # to the serializer
    kwargs['context'] = self.get_serializer_context()
    return serializer_class(*args, **kwargs)
    @laceynwilliams | @revsys

    View Slide

  26. def get_serializer_context(self):
    # Override this method to add more stuff
    # to the serializer context
    return {
    'request': self.request,
    'format': self.format_kwarg,
    'view': self
    }
    @laceynwilliams | @revsys

    View Slide

  27. When to override get_serializer_context():
    When you need to add something to the
    context so your serializer can use it
    @laceynwilliams | @revsys

    View Slide

  28. In your methods, you can run:
    serializer = self.get_serializer()
    instead of accessing your serializer directly.
    @laceynwilliams | @revsys

    View Slide

  29. Methods that come with the mixins for
    ModelViewSet
    @laceynwilliams | @revsys

    View Slide

  30. @laceynwilliams | @revsys

    View Slide

  31. class CreateModelMixin:
    def create(self, request, *args, **kwargs):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)
    headers = self.get_success_headers(serializer.data)
    return Response(
    serializer.data, status=status.HTTP_201_CREATED, headers=headers
    )
    @laceynwilliams | @revsys

    View Slide

  32. class CreateModelMixin:
    def create(self, request, *args, **kwargs):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)
    headers = self.get_success_headers(serializer.data)
    return Response(
    serializer.data, status=status.HTTP_201_CREATED, headers=headers
    )
    @laceynwilliams | @revsys

    View Slide

  33. class CreateModelMixin:
    def create(self, request, *args, **kwargs):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)
    headers = self.get_success_headers(serializer.data)
    return Response(
    serializer.data, status=status.HTTP_201_CREATED, headers=headers
    )
    @laceynwilliams | @revsys

    View Slide

  34. class CreateModelMixin:
    def perform_create(self, serializer):
    serializer.save()
    @laceynwilliams | @revsys

    View Slide

  35. class CreateModelMixin:
    def create(self, request, *args, **kwargs):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)
    headers = self.get_success_headers(serializer.data)
    return Response(
    serializer.data, status=status.HTTP_201_CREATED, headers=headers
    )
    @laceynwilliams | @revsys

    View Slide

  36. ... Why did you show me a bunch of source
    code?
    @laceynwilliams | @revsys

    View Slide

  37. "I want my books to have a lot of data for the
    detail endpoints, but only some of the data for
    the list endpoint. Can I do that?"
    @laceynwilliams | @revsys

    View Slide

  38. from rest_framework.permissions import AllowAny
    from rest_framework.viewsets import ModelViewSet
    from .models import Book
    from .serializers import BookDetailSerializer, BookListSerializer
    class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookDetailSerializer
    permission_classes = [AllowAny]
    def get_serializer_class(self):
    if self.action in ["list"]:
    return BookListSerializer
    return super().get_serializer_class()
    @laceynwilliams | @revsys

    View Slide

  39. "A#er I create a book, I want to use a different,
    more detailed serializer to return it."
    @laceynwilliams | @revsys

    View Slide

  40. Reminder
    class CreateModelMixin:
    def create(self, request, *args, **kwargs):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)
    headers = self.get_success_headers(serializer.data)
    return Response(
    serializer.data, status=status.HTTP_201_CREATED, headers=headers
    )
    @laceynwilliams | @revsys

    View Slide

  41. Reminder
    class CreateModelMixin:
    def create(self, request, *args, **kwargs):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)
    headers = self.get_success_headers(serializer.data)
    return Response(
    serializer.data, status=status.HTTP_201_CREATED, headers=headers
    )
    @laceynwilliams | @revsys

    View Slide

  42. from rest_framework.permissions import AllowAny
    from rest_framework.viewsets import ModelViewSet
    from .models import Book
    from .serializers import BookSerializer, BookCreatedSerializer
    class BookViewSet(ModelViewSet):
    ...
    def create(self, request, *args, **kwargs):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    # self.perform_create(serializer)
    instance = serializer.save()
    return_serializer = BookCreatedSerializer(instance)
    headers = self.get_success_headers(return_serializer.data)
    return Response(
    return_serializer.data, status=status.HTTP_201_CREATED, headers=headers
    )
    @laceynwilliams | @revsys

    View Slide

  43. from rest_framework.permissions import AllowAny
    from rest_framework.viewsets import ModelViewSet
    from .models import Book
    from .serializers import BookSerializer, BookCreatedSerializer
    class BookViewSet(ModelViewSet):
    ...
    def create(self, request, *args, **kwargs):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    # self.perform_create(serializer)
    instance = serializer.save()
    return_serializer = BookCreatedSerializer(instance)
    headers = self.get_success_headers(return_serializer.data)
    return Response(
    return_serializer.data, status=status.HTTP_201_CREATED, headers=headers
    )
    @laceynwilliams | @revsys

    View Slide

  44. from rest_framework.permissions import AllowAny
    from rest_framework.viewsets import ModelViewSet
    from .models import Book
    from .serializers import BookSerializer, BookCreatedSerializer
    class BookViewSet(ModelViewSet):
    ...
    def create(self, request, *args, **kwargs):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    # self.perform_create(serializer)
    instance = serializer.save()
    return_serializer = BookCreatedSerializer(instance)
    headers = self.get_success_headers(return_serializer.data)
    return Response(
    return_serializer.data, status=status.HTTP_201_CREATED, headers=headers
    )
    @laceynwilliams | @revsys

    View Slide

  45. "But I don't need ALL those endpoints. Only
    most of them."
    @laceynwilliams | @revsys

    View Slide

  46. Do everything except delete
    from rest_framework.generics import GenericAPIView
    from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin,
    UpdateModelMixin, ListModelMixin
    # import models and serializers...
    class BookViewSet(
    CreateModelMixin,
    RetrieveModelMixin,
    UpdateModelMixin,
    ListModelMixin,
    GenericAPIView
    ):
    ...
    @laceynwilliams | @revsys

    View Slide

  47. DRF mixes and matches for
    you
    ✓ UpdateAPIView = UpdateModelMixin + GenericAPIView
    ✓ CreateAPIView
    ✓ ListAPIView
    ✓ RetrieveAPIView
    ✓ DestroyAPIView
    ✓ ReadOnlyModelViewSet
    ✓ RetrieveDestroyAPIView
    ✓ RetrieveUpdateDestroy
    APIView
    ✓ ListCreateAPIView
    ✓ RetrieveUpdateAPIView
    @laceynwilliams | @revsys

    View Slide

  48. "Can we have an endpoint that just shows our
    featured books?"
    @laceynwilliams | @revsys

    View Slide

  49. from rest_framework.decorators import action
    class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookDetailSerializer
    def get_serializer_class(self):
    if self.action in ["list", "featured"]:
    return BookListSerializer
    return super().get_serializer_class()
    @action(detail=False, methods=["get"])
    def featured(self, request):
    books = self.get_queryset().filter(featured=True)
    serializer = self.get_serializer(books, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)
    @laceynwilliams | @revsys

    View Slide

  50. from rest_framework.decorators import action
    class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookDetailSerializer
    def get_serializer_class(self):
    if self.action in ["list", "featured"]:
    return BookListSerializer
    return super().get_serializer_class()
    @action(detail=False, methods=["get"])
    def featured(self, request):
    books = self.get_queryset().filter(featured=True)
    serializer = self.get_serializer(books, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)
    @laceynwilliams | @revsys

    View Slide

  51. url =
    GET /books/featured/
    @laceynwilliams | @revsys

    View Slide

  52. from rest_framework.decorators import action
    class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookDetailSerializer
    def get_serializer_class(self):
    if self.action in ["list", "featured"]:
    return BookListSerializer
    return super().get_serializer_class()
    @action(detail=False, methods=["get"])
    def featured(self, request):
    books = self.get_queryset().filter(featured=True)
    serializer = self.get_serializer(books, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)
    @laceynwilliams | @revsys

    View Slide

  53. from rest_framework.decorators import action
    class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookDetailerializer
    def get_serializer_class(self):
    if self.action in ["list", "featured"]:
    return BookListSerializer
    return super().get_serializer_class()
    @action(detail=False, methods=["get"])
    def featured(self, request):
    books = self.get_queryset().filter(featured=True)
    serializer = self.get_serializer(books, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)
    @laceynwilliams | @revsys

    View Slide

  54. from rest_framework.decorators import action
    class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookDetailerializer
    def get_serializer_class(self):
    if self.action in ["list", "featured"]:
    return BookListSerializer
    return super().get_serializer_class()
    @action(detail=False, methods=["get"])
    def featured(self, request):
    books = self.get_queryset().filter(featured=True)
    serializer = self.get_serializer(books, many=True)
    return Response(serializer.data, status=status.HTTP_200_OK)
    @laceynwilliams | @revsys

    View Slide

  55. Thank you!
    To Tom Chris*e and the maintainers of DRF.
    To Vinta So*ware and the maintainers of Classy DRF.
    To Beki Post and Jeff Triple3 for their help with this talk.
    To Vancouver PyLadies for le4ng me preview this talk with
    you.
    To PyCascades!
    @laceynwilliams | @revsys

    View Slide


  56. [email protected]
    "
    @laceynwilliams
    #
    laceyhenschel.com
    #
    revsys.com
    @laceynwilliams | @revsys

    View Slide