user

Development, September 13th, 2017

Simple Nested API Using Django REST Framework

Simple Nested API Using Django REST Framework

In this article you will learn how to build a simple 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 djangorestframework

Let’s create a project for this demo:

$ django-admin startproject demo && cd demo

 

Why should you even care?

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.

 

Models

First, we need some interconnected Models to wrap our API around.

$ ./manage.py startapp shelf

Next, add our app and REST Framework to settings.py:

INSTALLED_APPS = [
...
    'rest_framework',
    'shelf',
]

Finally, create the models:

# shelf/models.py

from django.db import models


class 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}'

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:

# shelf/serializers.py

from rest_framework.serializers import ModelSerializer
from .models import Author, Book


class AuthorSerializer(ModelSerializer):
    class Meta:
        model = Author
        fields = ('id', 'first_name', 'last_name')


class BookSerializer(ModelSerializer):
    class Meta:
        model = Book
        fields = ('id', 'author', 'title')

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.

# shelf/views.py

from rest_framework.viewsets import ModelViewSet
from .serializers import AuthorSerializer, BookSerializer
from .models import Author, Book


class AuthorViewSet(ModelViewSet):
    serializer_class = AuthorSerializer
    queryset = Author.objects.all()


class BookViewSet(ModelViewSet):
    serializer_class = BookSerializer
    queryset = Book.objects.all()

The final step is to create a basic routing for the API and connect the ViewSets:

# demo/api.py

from rest_framework.routers import DefaultRouter
from shelf.views import AuthorViewSet, BookViewSet


router = DefaultRouter()

router.register('authors', AuthorViewSet)
router.register('books', BookViewSet)



# demo/urls.py


from django.conf.urls import url, include
from django.contrib import admin
from .api import router


urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^api/', include(router.urls))
]

Let’s test the API behavior.

$ ./manage.py runserver

Then go to http://127.0.0.1:8000/api/ and create some entries.

 

author list

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 API.

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-extensions

The 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:

# shelf/views.py
from 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()

Then we’ll need to extend our DefaultRouter:

# demo/api.py

from rest_framework_extensions.routers import NestedRouterMixin
...
class NestedDefaultRouter(NestedRouterMixin, DefaultRouter):
    pass

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.

# demo/api.py

...
router = NestedDefaultRouter()

authors_router = router.register('authors', AuthorViewSet)
authors_router.register(
    'books', BookViewSet,
    base_name='author-books',
    parents_query_lookups=['author'])
...

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.

# shelf/models.py


class Edition(models.Model):
    book = models.ForeignKey(Book)
    year = models.PositiveSmallIntegerField()

    def __str__(self):
        return f'{self.book} edition {self.year}'



# shelf/serializers.py
from .models import Edition
...
class EditionSerializer(ModelSerializer):
    class Meta:
        model = Edition
        fields = ('id', 'book', 'year')


# shelf/views.py
from .serializers import EditionSerializer
from .models import Edition
...
class EditionViewSet(NestedViewSetMixin, ModelViewSet):
    serializer_class = EditionSerializer
    queryset = Edition.objects.all()


# demo/api.py
from 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']
           )

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 runserver

Open 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 create your own nested API using Django REST Framework. Please let me know in the comments what you think and if you want me to present another excerpt from the spellbook 😉

The next one will be about summoning helper routes for list and detail views (@list_route and @detail_route decorators).

Related articles:

Cover image source: Flickr

D K
  • Michał Krawczak

    Really good article. I had always problem with nested APIs in Django. Thanks for examples how to use it and I cannot wait for a next story about list and detail views.
    Thanks Dominik, cheers!

  • Harro van der Klauw

    Good article, we have been using https://github.com/alanjds/drf-nested-routers for this, which takes a bit of a different approach.

    Does this one handle the click-through in the browsable API? (Aka, being able to click to books from the author instance endpoint) Or is that something you would also need to do manually?

  • https://www.lewiscowles.co.uk/ Lewis Cowles

    The article looks cool. I suppose I just question the pattern of nested API’s and routing structures for potentially unattended access systems (what I use API’s for).

    To get to authors books in this example you need to know a synthetic PK. To get to editions of released books you need a synthetic PK and the related synthetic PK.

    It is very cool if applied to non-relational data-sources (which often have such problems anyway)

  • Pingback: 9 Answers For People Learning How to Code()

View Comments (4) ...