Build a RESTAPI using nested serializers in Django-Rest-Framework

Startup up a new project …mkdir bookingAPIcd bookingAPI# create virtual environment and activate itpython -m venv venvsource venv/bin/activate# install necessary dependenciespip install django djangorestframework coreapi coreapi-cli MarkdownPyaml# create django projectdjango-admin startproject mysite# Make initial migrations and runserverpython manage.

py makemigrationspython manage.

py runserver # Now let this server be running and open another terminal window# and enter the virtual env again.

# This is a new terminal window# Make an api appdjango-admin startapp api .

Models.

pyWe will create our models.

This should not be too difficult to understand.

Key is understanding where to place the foreign keys.

Make your code look like thisfrom django.

db import modelsclass Sport(models.

Model): name = models.

CharField(max_length=100) def __str__(self): return self.

nameclass Market(models.

Model): name = models.

CharField(max_length=100) sport = models.

ForeignKey(Sport,related_name='markets', on_delete =models.

CASCADE) def __str__(self): return self.

name + ' | ' + self.

sport.

nameclass Selection(models.

Model): name = models.

CharField(max_length=100) odds = models.

FloatField() market = models.

ForeignKey(Market,related_name='selections', on_delete =models.

CASCADE) def __str__(self): return self.

nameclass Match(models.

Model): name = models.

CharField(max_length=100) startTime = models.

DateTimeField() sport = models.

ForeignKey(Sport, related_name='matches', on_delete =models.

CASCADE) market = models.

ForeignKey(Market, related_name='matches', on_delete =models.

CASCADE) class Meta: ordering = ('startTime',) verbose_name_plural = 'Matches' def __str__(self): return self.

name# Run the migrations againpython manage.

py makemigration apipython manage.

py migrateNow let’s add our installed apps, filters and rest framework in our settings.

pyINSTALLED_APPS = [ 'api.

apps.

ApiConfig', .

# Make sure to include the default installed apps here.

'rest_framework', 'django_filters',].

REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': ('django_filters.

rest_framework.

DjangoFilterBackend',), 'DEFAULT_PAGINATION_CLASS': 'rest_framework.

pagination.

PageNumberPagination', 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.

permissions.

AllowAny',), 'PAGE_SIZE': 10, }SerializersTo implement rest api we need something called Serializers.

You might know django model forms.

Same concept can be used in serializers as well.

They refer to the models and present data stored in json format.

Create a file called serializers.

py in the api app and make it look like belowfrom api.

models import Match, Sport, Market, Selectionfrom rest_framework import serializersclass SportSerializer(serializers.

ModelSerializer): class Meta: model = Sport fields = ('id', 'name')class SelectionSerializer(serializers.

ModelSerializer): class Meta: model = Selection fields = ('id', 'name', 'odds')class MarketSerializer(serializers.

ModelSerializer): selections = SelectionSerializer(many=True) class Meta: model = Market fields = ('id', 'name','selections')class MatchListSerializer(serializers.

ModelSerializer): class Meta: model = Match fields = ('id', 'url', 'name', 'startTime')class MatchDetailSerializer(serializers.

ModelSerializer): sport = SportSerializer() market = MarketSerializer() class Meta: model = Match fields = ('id', 'url', 'name', 'startTime', 'sport', 'market')Note howin the MatchDetail serializer we refer to the Sport and Market serializer.

Here we are doing nesting of serializers.

Say like a detailed tree like structure.

A match serializer itself will carry all the details.

We use a List and a Detail Serializer for the Match object.

This is optional.

When listing all the matches ( might be in hundreds ) we might not be interested to see all the details.

Routersdjango-rest framework has this amazing concept of router which handles all url configurations for us automatically.

See how we import it, instantiate is and register our endpoint — `match` with it.

It takes care of the rest.

In the main projects urls.

py we shall add thisfrom api import viewsfrom django.

contrib import adminfrom django.

urls import include, pathfrom rest_framework.

routers import DefaultRouterfrom rest_framework.

schemas import get_schema_viewfrom rest_framework.

documentation import include_docs_urlsrouter = DefaultRouter()router.

register(r'match', views.

MatchViewSet)schema_view = get_schema_view(title='Bookings API', description='An API to book matches or update odds.

')urlpatterns = [ path('admin/', admin.

site.

urls), path('api/', include(router.

urls)), path('schema/', schema_view), path('docs/', include_docs_urls(title='Bookings API'))]SchemaHelps other machines to understand your api.

Its a simple roadmap of what endpoints are available and how to send data.

django rest framework makes it easy for us.

We just import and register the urlpattern.

DocumentationOther developers need to understand how to use your API.

So API documentation is a must.

There are many tools but I always prefer something simple and lucid.

To start with we call the include_docs_urls and register it with the urlpattern and it gives us some ready-made documentation.

ViewsetsWe can implement our views using functions or class based views but to go a one step further would be to use viewsets.

Viewsets provide great abstraction and make things much easier especially if your api project is large with many endpoints and you want consistency throughout your project.

Make your views.

py look like thisfrom api.

models import Match, Sport, Selection, Marketfrom api.

serializers import MatchListSerializer, MatchDetailSerializerfrom django_filters.

rest_framework import DjangoFilterBackendfrom rest_framework import status, viewsetsfrom rest_framework.

filters import OrderingFilterfrom rest_framework.

response import Responseclass MatchViewSet(viewsets.

ModelViewSet): """ retrieve: Return the given match.

list: Return a list of all the existing matches.

create: Create a new match instance.

""" queryset = Match.

objects.

all() serializer_class = MatchListSerializer # for list view detail_serializer_class = MatchDetailSerializer # for detail view filter_backends = (DjangoFilterBackend, OrderingFilter,) ordering_fields = '__all__' def get_serializer_class(self): """ Determins which serializer to user `list` or `detail` """ if self.

action == 'retrieve': if hasattr(self, 'detail_serializer_class'): return self.

detail_serializer_class return super().

get_serializer_class() def get_queryset(self): """ Optionally restricts the returned queries by filtering against a `sport` and `name` query parameter in the URL.

""" queryset = Match.

objects.

all() sport = self.

request.

query_params.

get('sport', None) name = self.

request.

query_params.

get('name', None) if sport is not None: sport = sport.

title() queryset = queryset.

filter(sport__name=sport) if name is not None: queryset = queryset.

filter(name=name) return queryset def create(self, request): """ to parse the incoming request and create a new match or update existing odds.

""" message = request.

data.

pop('message_type') # check if incoming api request is for new event creation if message == "NewEvent": event = request.

data.

pop('event') sport = event.

pop('sport') markets = event.

pop('markets')[0] # for now we have only one market selections = markets.

pop('selections') sport = Sport.

objects.

create(**sport) markets = Market.

objects.

create(**markets, sport=sport) for selection in selections: markets.

selections.

create(**selection) match = Match.

objects.

create(**event, sport=sport, market=markets) return Response(status=status.

HTTP_201_CREATED) # check if incoming api request is for updation of odds elif message == "UpdateOdds": event = request.

data.

pop('event') markets = event.

pop('markets')[0] selections = markets.

pop('selections') for selection in selections: s = Selection.

objects.

get(id=selection['id']) s.

odds = selection['odds'] s.

save() match = Match.

objects.

get(id=event['id']) return Response(status=status.

HTTP_201_CREATED) else: return Response(status=status.

HTTP_400_BAD_REQUEST)Note:The doc-strings that we write in our views will be used for our documentation so don’t miss on thatThe get_serializer_class helps us to find which serializer to use `list` or `detail` depending on the viewWe overwrite the get_query_set method to do filtering by certain keywords like `sport`, `name`, `ordering`We overwrite the default create method to analyze the incoming post json request from another api.

we need to parse the message type and find out what the motive isCreate a new matchOr update existing oddsWe should be able to see our interactive api at our local server.

Hit on http://localhost:8000/api/match and play around.

Without tests the code is broken by design.

Let’s write two simple tests in the api/tests.

pyfrom django.

urls import include, pathfrom api.

models import Match, Market, Selection, Sportfrom rest_framework import statusfrom rest_framework.

test import APITestCase, RequestsClient, URLPatternsTestCaseclass MatchTests(APITestCase): def test_create_match(self): """ Ensure we can create a new match object.

""" url = ('http://127.

0.

0.

1:8000/api/match/') data = { "id": 8661032861909884224, "message_type": "NewEvent", "event": { "id": 994839351740, "name": "Real Madrid vs Barcelona", "startTime": "2018-06-20 10:30:00", "sport": { "id": 221, "name": "Football" }, "markets": [ { "id": 385086549360973392, "name": "Winner", "selections": [ { "id": 8243901714083343527, "name": "Real Madrid", "odds": 1.

01 }, { "id": 5737666888266680774, "name": "Barcelona", "odds": 1.

01 } ] } ] } } response = self.

client.

post(url, data, format='json') self.

assertEqual(response.

status_code, status.

HTTP_201_CREATED) self.

assertEqual(Match.

objects.

count(), 1) self.

assertEqual(Match.

objects.

get().

name, 'Real Madrid vs Barcelona') def test_update_odds(self): """ Ensure we can update odds on a created match object.

""" url = ('http://127.

0.

0.

1:8000/api/match/') # first we create a match create_match = { "id": 8661032861909884224, "message_type": "NewEvent", "event": { "id": 994839351740, "name": "Real Madrid vs Barcelona", "startTime": "2018-06-20 10:30:00", "sport": { "id": 221, "name": "Football" }, "markets": [ { "id": 385086549360973392, "name": "Winner", "selections": [ { "id": 8243901714083343527, "name": "Real Madrid", "odds": 1.

01 }, { "id": 5737666888266680774, "name": "Barcelona", "odds": 1.

01 } ] } ] } } response = self.

client.

post(url, create_match, format='json') # now we update the odds on the same match update_odds = { "id": 8661032861909884224, "message_type": "UpdateOdds", "event": { "id": 994839351740, "name": "Real Madrid vs Barcelona", "startTime": "2018-06-20 10:30:00", "sport": { "id": 221, "name": "Football" }, "markets": [ { "id": 385086549360973392, "name": "Winner", "selections": [ { "id": 8243901714083343527, "name": "Real Madrid", "odds": 0.

5 }, { "id": 5737666888266680774, "name": "Barcelona", "odds": 1.

5 } ] } ] } } response = self.

client.

post(url, update_odds, format='json') self.

assertEqual([1.

5, 0.

5], [s.

odds for s in Selection.

objects.

all()]) self.

assertEqual(Match.

objects.

get().

name, 'Real Madrid vs Barcelona')This would be it for now.

Django-rest-framework has an amazing documentation and tutorial as well.

Do check it out.

.

. More details

Leave a Reply