Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

2c7f0a1020fbd01942166122190180f8?s=128

Lacey Williams Henschel

February 20, 2021
Tweet

Transcript

  1. What You Should Know About Django REST Framework Lacey Williams

    Henschel PyCascades, February 2021 @laceynwilliams | @revsys
  2. What You Should Know About Django REST Framework: ViewSets Edition!

    @laceynwilliams | @revsys
  3. ClassyDRF @laceynwilliams | @revsys

  4. @laceynwilliams | @revsys

  5. @laceynwilliams | @revsys

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

    @revsys
  7. ModelViewSet @laceynwilliams | @revsys

  8. ModelViewSet = set of views to: ✓ create ✓ retrieve

    ✓ update ✓ list ✓ destroy @laceynwilliams | @revsys
  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
  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
  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
  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
  13. A"ributes for your ModelViewSet @laceynwilliams | @revsys

  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
  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
  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
  17. Methods that come from GenericAPIView @laceynwilliams | @revsys

  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
  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
  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
  21. In your own methods, you can run: obj = self.get_object()

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

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

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

    of accessing your serializer directly. @laceynwilliams | @revsys
  29. Methods that come with the mixins for ModelViewSet @laceynwilliams |

    @revsys
  30. @laceynwilliams | @revsys

  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
  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
  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
  34. class CreateModelMixin: def perform_create(self, serializer): serializer.save() @laceynwilliams | @revsys

  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
  36. ... Why did you show me a bunch of source

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

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

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

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

    books?" @laceynwilliams | @revsys
  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
  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
  51. url = GET /books/featured/ @laceynwilliams | @revsys

  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
  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
  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
  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
  56. ✉ lacey@revsys.com " @laceynwilliams # laceyhenschel.com # revsys.com @laceynwilliams |

    @revsys