Technology

Simple Nested API Using Django REST Framework

In this article you will learn how to build a REST API using Django REST Framework. The code in this article was written with Python 3.6, Django 1.11 and DRF 3.6 in mind.Two of my wizard-friends found it difficult to create an API using Django REST Framework. Several curses had been cast before they turned to me for help. I decided to write a helpful spellbook of arcane incantations to summon a proper Django REST Framework (referred to as DRF) API. What follows is the first part of said grimoire translated to common speech.

Prerequisites

Python 3.6, Django 1.11 and Django Rest Framework 3.6 were used to construct spells contained here. It is also assumed that every command is run inside a virtualenv. If you’re not familiar with it, no problem, just use sudo pip instead of pip. If you don’t have Python 3.6 yet, you shall port (or remove) __str__ methods as they use new formatted string literals.$ pip install django djangorestframeworkLet’s create a Django project for this demo:$ django-admin startproject demo && cd demo

Why you should build a REST API using Django REST Framework?

Let’s consider a popular “library” approach. Flat API would have e.g. books and authors endpoints. Searching for books by particular author could then look like this: books/?author={author_id} and that’s quite OK.But many wizards find it much more logical to lay a nested structure to their API. The same query would then look like this: authors/{author_id}/books and so on. Many frontend tools support such layout automatically hence the need to construct such layout using DRF.Now, let's see how to build a REST API using Django REST Framework.

Models

First, we need some interconnected Models to wrap our API around. $ ./manage.py startapp shelfNext, add our app and REST Framework to settings.py:[code language="python"]INSTALLED_APPS = [...    'rest_framework',    'shelf',][/code]Finally, create the models:[code language="python"]# shelf/models.pyfrom django.db import modelsclass Author(models.Model):    first_name = models.CharField(max_length=20)    last_name = models.CharField(max_length=20)    def __str__(self):        return f'{self.first_name} {self.last_name}'class Book(models.Model):    title = models.CharField(max_length=60)    author = models.ForeignKey(Author)    def __str__(self):        return f'{self.title}'[/code]Remember to make migrations and apply them:$ ./manage.py makemigrations && ./manage.py migrate

Serializers

OK, time to start brewing our API.First we need to create some serializers to handle our data interchange (DRF uses JSON by default but you can change that to XML or YAML). Some people argue that this could be made automatically on the ViewSet level. Little do they know that making an API is much like creating a Form-View combo but on a different level. And hardly anyone complains about the Forms ;).That being said, let’s start with the serializers:[code language="python"]# shelf/serializers.pyfrom rest_framework.serializers import ModelSerializerfrom .models import Author, Bookclass AuthorSerializer(ModelSerializer):    class Meta:        model = Author        fields = ('id', 'first_name', 'last_name')class BookSerializer(ModelSerializer):    class Meta:        model = Book        fields = ('id', 'author', 'title')[/code]You’ll probably admit it wasn’t all that hard. Probably the worst part is that fields meta attribute is compulsory. You can use the magic value '__all__' but listing specific fields is recommended. It makes your API much safer.

Viewsets and routing

Now it’s time to write the basic viewsets. Note: Usually when I write api-only backend, I use views.py for API views. If you need to separate API views from “normal” web views, you can put this code into e.g. api.py - just remember to update imports in other files accordingly.[code language="python"]# shelf/views.pyfrom rest_framework.viewsets import ModelViewSetfrom .serializers import AuthorSerializer, BookSerializerfrom .models import Author, Bookclass AuthorViewSet(ModelViewSet):    serializer_class = AuthorSerializer    queryset = Author.objects.all()class BookViewSet(ModelViewSet):    serializer_class = BookSerializer    queryset = Book.objects.all()[/code]The final step is to create a basic routing for the API and connect the ViewSets:[code language="python"]# demo/api.pyfrom rest_framework.routers import DefaultRouterfrom shelf.views import AuthorViewSet, BookViewSetrouter = DefaultRouter()router.register('authors', AuthorViewSet)router.register('books', BookViewSet)# demo/urls.pyfrom django.conf.urls import url, includefrom django.contrib import adminfrom .api import routerurlpatterns = [    url(r'^admin/', admin.site.urls),    url(r'^api/', include(router.urls))][/code]Let’s test the API behavior.$ ./manage.py runserverThen go to http://127.0.0.1:8000/api/ and create some entries.

REST API using Django REST Framework



So far so good - we have a basic API we can use to list, create and edit our data in quite a RESTful way.

Nesting routers

Now it’s time for the main subject of this article - how to make a nested REST API using Django REST Framework.There are a couple packages that handle nesting logic. We will use DRF-Extensions as it is the most feature-rich package that we can use in a future how-to:$ pip install drf-extensionsThe first thing to do is to add a mixin to our ViewSets. It will ensure that the url params are correctly handled and the queryset filtered properly:[code language="python"]# shelf/views.pyfrom rest_framework_extensions.mixins import NestedViewSetMixin...class AuthorViewSet(NestedViewSetMixin, ModelViewSet):    serializer_class = AuthorSerializer    queryset = Author.objects.all()class BookViewSet(NestedViewSetMixin, ModelViewSet):    serializer_class = BookSerializer    queryset = Book.objects.all()[/code]Then we’ll need to extend our DefaultRouter:[code language="python"]# demo/api.pyfrom rest_framework_extensions.routers import NestedRouterMixin...class NestedDefaultRouter(NestedRouterMixin, DefaultRouter):    pass[/code]Now we can start nesting our routes for fun and profit.The NestedDefaultRouter we made allows us to create subrouters to register nested endpoints. It's almost as simple as registering normal routers. We only need to add two extra params so that the automation will know how to connect everything together.[code language="python"]# demo/api.py...router = NestedDefaultRouter()authors_router = router.register('authors', AuthorViewSet)authors_router.register(    'books', BookViewSet,    base_name='author-books',    parents_query_lookups=['author'])...[/code]Some things to keep in mind:

  • base_name needs to be unique across your API. It's the name that will be the root for url names used by reverse() function.
  • parents_query_lookups is a list of relations linking to parent models. These values are used as param names for filter() function. In our example this would be author on Book model: queryset = Book.objects.filter(author={value from url})

After these changes we can get a list of all books by a certain author. Reload your server and go to this url: http://127.0.0.1:8000/api/authors/2/books/

book list


Further nesting - here be dragons

The deeper you go with the nesting, the messier will parents_query_lookups get. Let's add the Edition to the Book to illustrate the problem.[code language="python"]# shelf/models.pyclass Edition(models.Model):    book = models.ForeignKey(Book)    year = models.PositiveSmallIntegerField()    def __str__(self):        return f'{self.book} edition {self.year}'# shelf/serializers.pyfrom .models import Edition...class EditionSerializer(ModelSerializer):    class Meta:        model = Edition        fields = ('id', 'book', 'year')# shelf/views.pyfrom .serializers import EditionSerializerfrom .models import Edition...class EditionViewSet(NestedViewSetMixin, ModelViewSet):    serializer_class = EditionSerializer    queryset = Edition.objects.all()# demo/api.pyfrom shelf.views import EditionViewSet...authors_router.register(    'books', BookViewSet,    base_name='author-books',    parents_query_lookups=['author']).register('editions',           EditionViewSet,            base_name='author-book-edition',            parents_query_lookups=['book__author', 'book']           )[/code]Notice how we chained another register() right after the first one. Note that if you have more endpoints to add on that level, you should instead do the same trick we did with authors_router. Please also notice how parents_query_lookups looks now. The Editions will be found using this filter:queryset = Edition.objects.filter(book__author={first value from url}, book={second value from url})Now you can add some Editions and check if everything is OK. Just remember about migrations:$ ./manage.py makemigrations && ./manage.py migrate$ ./manage.py runserverOpen an appropriate url and add some Editions (see Note below!).

edition list


Note

Django REST Framework will not filter the query sets for a built-in API browser. This means it will allow you e.g. to select any author even if you are on a specific author’s book list.

Wrap up

Now you should be able to build a REST API using Django REST FrameworkThe next one will be about summoning helper routes for list and detail views (@list_route and @detail_route decorators).Cover image source: Flickr

Read more

How to Create a Product Roadmap?
Getting started with react-spring: spring-physics, API, performance