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 djangorestframework

Let’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 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.

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

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!).



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 Framework

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

Cover image source: Flickr