Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

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
  2. ModelViewSet = set of views to: ✓ create ✓ retrieve

    ✓ update ✓ list ✓ destroy @laceynwilliams | @revsys
  3. 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
  4. Each *ModelMixin class has its own methods that perform the

    right ac4ons. CreateModelMixin has a create() method, instead of a post() method. @laceynwilliams | @revsys
  5. 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
  6. ...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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. In your own methods, you can run: obj = self.get_object()

    instead of looking up the object yourself. @laceynwilliams | @revsys
  14. 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
  15. When to override get_serializer_class(): When you want to use different

    serializers in different situa3ons @laceynwilliams | @revsys
  16. 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
  17. 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
  18. 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
  19. When to override get_serializer_context(): When you need to add something

    to the context so your serializer can use it @laceynwilliams | @revsys
  20. In your methods, you can run: serializer = self.get_serializer() instead

    of accessing your serializer directly. @laceynwilliams | @revsys
  21. 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
  22. 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
  23. 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
  24. 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
  25. ... Why did you show me a bunch of source

    code? @laceynwilliams | @revsys
  26. "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
  27. 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
  28. "A#er I create a book, I want to use a

    different, more detailed serializer to return it." @laceynwilliams | @revsys
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. "But I don't need ALL those endpoints. Only most of

    them." @laceynwilliams | @revsys
  35. 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
  36. DRF mixes and matches for you ✓ UpdateAPIView = UpdateModelMixin

    + GenericAPIView ✓ CreateAPIView ✓ ListAPIView ✓ RetrieveAPIView ✓ DestroyAPIView ✓ ReadOnlyModelViewSet ✓ RetrieveDestroyAPIView ✓ RetrieveUpdateDestroy APIView ✓ ListCreateAPIView ✓ RetrieveUpdateAPIView @laceynwilliams | @revsys
  37. "Can we have an endpoint that just shows our featured

    books?" @laceynwilliams | @revsys
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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