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.

You may also like: Constate Library - alternative to global state of the app management

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

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

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

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

# 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))]

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.

Worth checking: Dive into Javascript deeper with an explanation of Object.Prototype and classes

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

Then we’ll need to extend our DefaultRouter:

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

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.

Also read: How to use TypeScript Record Utility Type? Detailed guide

Simple REST API using Django Framework. 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