We have three API endpoints
/polls/
and/polls/<pk>/
/choices/
/vote/
They get the work done, but we can make our API more intuitive by nesting them correctly. Our redesigned urls look like this:
/polls/
and/polls/<pk>
/polls/<pk>/choices/
- to GET the choices for a specific poll, and to create choices for a specific poll. (Identified by the<pk>
)/polls/<pk>/choices/<choice_pk>/vote/
- to vote for the choice identified by<choice_pk>
under poll with<pk>
.
We will make changes to ChoiceList
and CreateVote
, because the /polls/
and /polls/<pk>
have not changed.
from rest_framework import generics
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from .models import Poll, Choice
from .serializers import PollSerializer, ChoiceSerializer, VoteSerializer
# ...
# PollList and PollDetail views
class ChoiceList(generics.ListCreateAPIView):
def get_queryset(self):
queryset = Choice.objects.filter(poll_id=self.kwargs["pk"])
return queryset
serializer_class = ChoiceSerializer
class CreateVote(APIView):
serializer_class = VoteSerializer
def post(self, request, pk, choice_pk):
voted_by = request.data.get("voted_by")
data = {'choice': choice_pk, 'poll': pk, 'voted_by': voted_by}
serializer = VoteSerializer(data=data)
if serializer.is_valid():
vote = serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
And change your urls.py to a nested structure.
#...
urlpatterns = [
path("polls/<int:pk>/choices/", ChoiceList.as_view(), name="choice_list"),
path("polls/<int:pk>/choices/<int:choice_pk>/vote/", CreateVote.as_view(), name="create_vote"),
]
You can see the changes by doing a GET to http://localhost:8000/polls/1/choices/
, which should give you.
[
{
"id": 1,
"votes": [],
"choice_text": "Flask",
"poll": 1
},
{
"id": 2,
"votes": [
],
"choice_text": "Django",
"poll": 1
}
]
You can vote for choice 2 of poll 1 by doing a POST to http://localhost:8000/polls/1/choices/2/vote/
with data {"voted_by": 1}
.
{
"id": 2,
"choice": 2,
"poll": 1,
"voted_by": 1
}
Lets get back to ChoiceList
.
# urls.py
#...
urlpatterns = [
# ...
path("polls/<int:pk>/choices/", ChoiceList.as_view(), name="choice_list"),
]
# apiviews.py
# ...
class ChoiceList(generics.ListCreateAPIView):
def get_queryset(self):
queryset = Choice.objects.filter(poll_id=self.kwargs["pk"])
return queryset
serializer_class = ChoiceSerializer
From the urls, we pass on pk
to ChoiceList
. We override the get_queryset
method, to filter on choices with this poll_id
, and let DRF handle the rest.
And for CreateVote
:
# urls.py
#...
urlpatterns = [
# ...
path("polls/<int:pk>/choices/<int:choice_pk>/vote/", CreateVote.as_view(), name="create_vote"),
]
# apiviews.py
# ...
class CreateVote(APIView):
def post(self, request, pk, choice_pk):
voted_by = request.data.get("voted_by")
data = {'choice': choice_pk, 'poll': pk, 'voted_by': voted_by}
serializer = VoteSerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
We pass on poll id and choice id. We subclass this from APIView
, rather than a generic view, because we completely customize the behaviour. This is similar to our earlier APIView
, wherein we are passing the data to a serializer, and saving or returning an error depending on whether the serializer is valid.
Our urls are looking good, and we have views with very little code duplication, but we can do better.
Recall that the /polls/
and /polls/<pk>/
urls require two view classes, PollList and PollDetail, with the same code for serializer and base queryset.
```py class PollList(generics.ListCreateAPIView):
queryset = Poll.objects.all() serializer_class = PollSerializer
- class PollDetail(generics.RetrieveDestroyAPIView):
- queryset = Poll.objects.all() serializer_class = PollSerializer
We can group these classes into a viewset, and connect them to the urls using a router.
This is what it will look like:
# urls.py
# ...
from rest_framework.routers import DefaultRouter
from .apiviews import PollViewSet
router = DefaultRouter()
router.register('polls', PollViewSet, basename='polls')
urlpatterns = [
# ...
]
urlpatterns += router.urls
# apiviews.py
# ...
from rest_framework import viewsets
from .models import Poll, Choice
from .serializers import PollSerializer, ChoiceSerializer, VoteSerializer
class PollViewSet(viewsets.ModelViewSet):
queryset = Poll.objects.all()
serializer_class = PollSerializer
There is no change at all to the urls or to the responses. You can verify this by doing a GET to
/polls/
and /polls/<pk>/
.
We have seen 4 ways to build API views until now
- Pure Django views
APIView
subclassesgenerics.*
subclassesviewsets.ModelViewSet
So which one should you use when? My rule of thumb is,
- Use
viewsets.ModelViewSet
when you are going to allow all or most of the CRUD operations on a model. - Use
generics.*
when you only want to allow some operations on a model - Use
APIView
when you want to completely customize the behaviour.
In the next chapter, we will look at adding access control to our apis.