Quick Start¶
Note
This quickstart guide is based on the example Django project located at
/example
in the repository.
Create a New Django Project¶
Let’s get started with a fresh Django project:
$ django-admin startproject example
And let’s add an app for our API:
$ cd example
$ django-admin startapp api
Settings¶
drf_jsonapi
doesn’t have any settings of its own, but because it is built
on top of Django Rest Framework you’ll need to specify some settings to direct
DRF to leverage the special classes that drf_jsonapi
provides.
Make the following changes to your settings.py
file:
INSTALLED_APPS = [
...
"api",
"rest_framework",
]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (),
"DEFAULT_PERMISSION_CLASSES": (),
"UNAUTHENTICATED_USER": None,
"EXCEPTION_HANDLER": "drf_jsonapi.exception_handlers.jsonapi_exception_handler",
"DEFAULT_RENDERER_CLASSES": (
"drf_jsonapi.renderers.JSONRenderer",
"drf_jsonapi.renderers.BrowsableAPIRenderer",
),
"DEFAULT_PARSER_CLASSES": (
"drf_jsonapi.parsers.JSONAPIParser",
"rest_framework.parsers.JSONParser",
),
}
There are several things going on here. First, we’re adding our new api
app
and rest_framework
to the list of installed apps.
Next, we’re specifying some settings for django rest framework. First we’re disabling authentication on the API. Obviously this isn’t something you’d want to do in production but it’s fine for this example and makes things much simpler.
Note
See the Django Rest Framework Documentation for more information on securing your API.
The next part overrides the default exception handler, renderer, and parser
classes to use the drf_jsonapi
versions.
Models¶
While not required, drf_jsonapi
is designed to work conveniently with
Django models and the Django ORM. In this example we’ll create some models
with many-to-many relationships to show off JSON-API’s ability to work
with graph-like data structures.
Add the following to your api/models.py
file:
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_length=128)
class Author(models.Model):
name = models.CharField(max_length=128)
class Book(models.Model):
title = models.CharField(max_length=128)
authors = models.ManyToManyField(Author, related_name="books")
publisher = models.ForeignKey(
Publisher, related_name="books", on_delete=models.CASCADE
)
Serializers¶
Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON. Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.
Note
For more information on serializers see the Django Rest Framework documentation here.
drf-jsonapi
provides the ResourceModelSerializer
base class that you
can use to create your own serializers. If you’re using Django models and the
Django ORM (as we are here) then your serializers can be very simple.
If you’re building an API for other data structures such as in-memory data or
data fetched from a NOSQL database you’ll want to use the
ResourceSerializer
base class and manually define how the data should be
serialized and deserialized.
Let’s start with the serializer for the Publisher model. Create a new file
at api/serializers.py
and add the following:
from drf_jsonapi.serializers import ResourceModelSerializer
from drf_jsonapi.relationships import RelationshipHandler
from api.models import Publisher, Author, Book
class PublisherSerializer(ResourceModelSerializer):
class Meta:
type = "publisher"
basename = "publishers"
model = Publisher
id_field = "pk"
fields = ("name",)
@staticmethod
def define_relationships():
return {"books": RelationshipHandler(BookSerializer, "books", many=True)}
If you’re familiar with Django Rest Framework you might have noticed that this is very similar to ModelSerializer with some additional attributes.
- The
type
attribute describes the JSON-API resource type. basename
describes the base path for this resources. This is an optional attribute that, if omitted, will default to the resource type. I prefer types to be singular and basenames to be plural so this gives you that option.model
is simply a reference to the Model class that this serializer is for.id_field
is the model field that should be used as the identifier for each resource. This is also optional and will default topk
but it’s useful if you have an alternative field (such as a UUID field) that should be used instead ofpk
.fields
is a tuple (or list) of fields that should be included as attributes for the resource. One thing to note is that you don’t want to include any foreign key relationships here. These should be direct attributes of the model.
The define_relationships()
static method is used to describe how this
resource relates to other resources. It should return a dict
where the
key is the name of the relationship (“books” in this case) and the value is an
instance of RelationshipHandler
. The RelationshipHandler
constructor
takes 3 arguments: the related resources serializer class, the lookup field
for the relationship, and whether it’s a “To-Many” relationship.
For simple relationships using the Django ORM this is all you need. For more complex relationships you’ll want to create your own RelationshipHandler class by sub-classing RelationshipHandler.
Using these same concepts we can flesh out the serializers for the other models:
class AuthorSerializer(ResourceModelSerializer):
class Meta:
type = "author"
basename = "authors"
model = Author
id_field = "pk"
fields = ("name",)
@staticmethod
def define_relationships():
return {"books": RelationshipHandler(BookSerializer, "books", many=True)}
class BookSerializer(ResourceModelSerializer):
class Meta:
type = "book"
basename = "books"
model = Book
id_field = "pk"
fields = ("title",)
@staticmethod
def define_relationships():
return {
"authors": RelationshipHandler(AuthorSerializer, "authors", many=True),
"publisher": RelationshipHandler(PublisherSerializer, "publisher"),
}
Views¶
Django Rest Framework has a concept called Viewsets
which are a form of class-based View. drf_jsonapi
expands on this concept
with specialized ViewSets
for handling JSON-API requests and responses.
Let’s get started with a basic ViewSet for Publishers. Edit your
api/views.py
and add the following code:
from drf_jsonapi.viewsets import ReadWriteViewSet
from api.models import Publisher
from api.serializers import PublisherSerializer
class PublisherViewSet(ReadWriteViewSet):
serializer_class = PublisherSerializer
collection = Publisher.objects.all()
This is the bare minimum implementation of a ViewSet
. We’re sub-classing
ReadWriteViewSet
here. (There is also a ReadOnlyViewSet
that does not
allow POST, PATCH, or DELETE requests.) We declare two class attributes: The
serializer_class
of the resource and the default collection
which, in
this case, is a queryset of all Publisher
objects.
This ViewSet
will work fine for now but it is very limited. We’ll come back
to this later to make some improvements.
The Router¶
At this point we have our Models to describe our data. We’ve also written some serializers to define how render those models according to JSON-API spec. We’ve also written a simple ViewSet for Publishers to handle requests and responses.
Now we need to bring it all together by writing some URL routing rules so we can try out our API!
Edit your api/urls.py
file and add the following:
from drf_jsonapi.routers import Router
from api.views import PublisherViewSet
router = Router(trailing_slash=False)
router.register(PublisherViewSet)
urlpatterns += router.urls
You’ll also want to edit example/urls.py
to include these urls:
from django.urls import path, include
urlpatterns = [
path("", include("api.urls"))
]
The Router
class is a sub-class of Django Rest Framework’s
DefaultRouter
class. This class does all the work of creating the url
configurations so requests to /publishers
and /publishers/1
are
correctly routed to your ViewSets
.
To do this just create an instance of Router
and register your ViewSets
with it. Then you populate urlpatterns
with router.urls
and you’re
ready to go.
Testing out your API¶
At this point we’re almost ready to try out our API. Before we can do that though we need a database so we can persist some data. Fortunately Django provides out-of-the-box support for Sqlite. That’s perfect for our little example.
Let’s create our migrations so we can set up the database schema:
$ python manage.py makemigrations api
Then we need to apply the migrations:
$ python manage.py migrate
Now we have an empty database which is somewhat boring. We can use Django’s
fixture support to load in some sample data. Copy
/example/api/fixtures/example.json
to your project and load it into the
database with the following command:
$ python manage.py loaddata example
Now that we have some data let’s test out the API! First we need to launch the development server:
$ python manage.py runserver
You should now have a development server up and running at
http://127.0.0.1:8000/
. Load that up in a browser and take a look. You
should see something like this:
What you’re seeing is Django Rest Framework’s browsable API. This is the root
view which lists links for each top-level resource. At the moment we only have
a single resource: /publishers
. Follow the link to see the list of
publishers. You should see something like this:
Here’s a list of all Publishers in a nice JSON-API response.
Compound Documents¶
One of the nice features of JSON-API is the ability to include related
resources into compound documents.
drf-jsonapi
makes it easy to support this feature and we’ve already
configured our serializers to support these included relationships (that’s what
the define_relationships
and RelationshipHandlers
are for).
To try this out just add ?include=books
to the Publisher list url. This
tells the API to include related books in the response. You should see two
changes to the response. First, each Publisher resource now contains a
collection of ResourceIdentifiers
(basically just an ID and a type) for each related book.
Also, each Book resource now appears in the included
section of the of the
response.
N+1 Query Problems¶
With great power comes great responsibility. As cool as it is to automatically include related books there is a problem with our implementation that we need to fix. To understand what that problem is we need to take a look at the database queries we’re making to create our response.
Update api/views.py
with the following changes:
from drf_jsonapi.mixins import DebugMixin
class PublisherViewSet(DebugMixin, ReadWriteViewSet):
...
The DebugMixin
adds some additional metadata to responses. Reload
http://127.0.0.1/publishers
and scroll to the bottom. You should see this:
Ouch! That’s a lot of queries. We’re executing a separate query to fetch the related books for every publisher. This is the classic N+1 Query Problem and it can cause major performance problems in a production app.
We can solve this problem by eagerly loading all the related books in advance if we know we are going to need them. For example:
class PublisherViewSet(DebugMixin, ReadWriteViewSet):
serializer_class = PublisherSerializer
def get_collection(self, request):
collection = Publisher.objects.all()
if "books" in request.include:
collection = collection.prefetch_related("books")
return collection
First off, we’re replacing the collection
class attribute with a method
called get_collection
which accepts the current request as an argument.
This will allow us to check which relationships are to be included so we can
modify our collection QuerySet
to prefetch any related books.
Reload your page and you should see way fewer database queries.
Ah! Much better!