Walkthrough

Let’s create an application from scratch. We’re going to build a music sharing app using Django with a GraphQL API powered by Graphene and DjraphQL.

Project setup

Ready? Let’s dive in!

Virtual environment

Let’s create and activate our virtual environment:

# Optional: ensure we're using Python 3 via -p
virtualenv -p python3.7 .venv
source .venv/bin/activate

Next, let’s install the dependencies we’ll need. You can use your dependency manager of choice. For simplicity, we’ll use pip.

pip install django graphene graphene_django djraphql

Django setup

Let’s create our Django project, which we will call songster.

.venv/bin/django-admin startproject songster

And now we’ll create our Django app within our project. Let’s call it songs.

cd songster
python manage.py startapp songs

Next, we need to add our new app’s configuration to INSTALLED_APPS in settings.py.

1
2
3
4
5
   INSTALLED_APPS = [
     'songs.apps.SongsConfig',
     'django.contrib.admin',
     'django.contrib.auth',
     'django.contrib.contenttypes',

Creating Django models

Now we’ll create our models in songster/songs/models.py.

from django.db import models

class Label(models.Model):
    name = models.CharField(null=False, max_length=512)
    established_year = models.IntegerField()

class Artist(models.Model):
    name = models.CharField(null=False, max_length=512)
    label = models.ForeignKey(Label, null=True, related_name="artists", on_delete=models.CASCADE)

class Album(models.Model):
    title = models.CharField(null=False, max_length=512)
    released_date = models.DateField()
    artist = models.ForeignKey(Artist, related_name="albums", on_delete=models.CASCADE)

class Song(models.Model):
    album = models.ForeignKey(Album, null=True, related_name="songs", on_delete=models.CASCADE)
    title = models.CharField(null=False, max_length=512)
    duration_seconds = models.PositiveIntegerField(null=False)
    collaborators = models.ManyToManyField(Artist)

Build the migration files via makemigrations.

python manage.py makemigrations songs

Finally, run the migration.

python manage.py migrate

The output from the last command should display something like Applying songs.0001_initial... OK.

Seeding data

It’s convenient to have a bit of data to test with. Django’s fixtures feature makes that easy. First, create a file to hold our fixture data:

mkdir songs/fixtures
touch songs/fixtures/songs.json

Copy the content from this Github gist and paste it into songs.json.

Then, use the loaddata command:

python manage.py loaddata songs

Build a schema

Now that we’ve created our Django application and have built a few models for our app, we can turn toward the fun part: creating our GraphQL API!

Entity classes

To tell djraphql how to build our GraphQL schema, we must define classes that inherit from Entity, which we can use to customize our API.

But first, where should we define these classes?

Our songs app seems like a natural place and is certainly reasonable. But as our startup grows and we add more apps to our project (each of which with their own set of entity classes), our entities would end up strewn across the project.

To ensure that our GraphQL-specific logic stays in a single place in our project, let’s create a new app called graphql_api and place our entities there, along with any other GraphQL-specific logic.

python manage.py startapp graphql_api

In the newly created graphql_api directory, create an entities.py file:

from songs.models import *
from graphene import Schema
from djraphql import SchemaBuilder, Entity, AllModelFields

class LabelEntity(Entity):
    class Meta:
        model = Label
        fields = (AllModelFields(),)

class ArtistEntity(Entity):
    class Meta:
        model = Artist
        fields = (AllModelFields(),)

class AlbumEntity(Entity):
    class Meta:
        model = Album
        fields = (AllModelFields(),)

class SongEntity(Entity):
    class Meta:
        model = Song
        fields = (AllModelFields(),)

schema_builder = SchemaBuilder()
schema_builder.register_entity_classes(
    LabelEntity, ArtistEntity, AlbumEntity, SongEntity)

schema = Schema(query=schema_builder.QueryRoot)

Let’s walk through the above code.

First off, imports. We’re grabbing the model classes we want to build our schema around. We also are importing the Schema class from the graphene library, which is used to build the schema with the types generated by DjraphQL. Our last two imports are the Entity and SchemaBuilder classes from DjraphQL.

We then define four classes, each inheriting from Entity. Each class wraps a model we want to include in our GraphQL schema. For demonstration purposes, we’ll use the AllModelFields helper to conveniently expose all fields on eachs model.

We then build our Graphene types by instantiating a SchemaBuilder and registering our entity classes.

Finally, we instantiate the Schema, passing schema_builder.QueryRoot as the query argument.

Queries

Let’s execute our first GraphQL query. Open the Django shell via:

python manage.py shell

Once in the shell, execute the following commands:

from graphql_api.entities import schema
schema.execute("query { ArtistsMany { id name } }").data
# {'ArtistsMany': [{'id': 1, 'name': 'The Beatles'}, {'id': 2, 'name': 'George Martin'}, {'id': 3, 'name': 'Jimi Hendrix'}]}

And just like that, we’ve got a working schema!

Mutations

In order to perform mutations, we need to briefly touch on the permissions model that DjraphQL uses.

The Entity class has an access_permissions field that can be used to control whether or not the schema will contain types corresponding to each C.R.U.D. (create, read, update, delete) operation.

The default value is access_permissions = (R,). So, if left unaltered, all types will be readable, but not mutable. Let’s change that.

Update the SongEntity class in entities.py like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from songs.models import *
from graphene import Schema
from djraphql import SchemaBuilder, Entity
from djraphql.access_permissions import C, R, U, D

...

class SongEntity(Entity):
   class Meta:
       model = Song
       fields = (AllModelFields(),)

   access_permissions = (C, R, U, D)

...

schema = Schema(query=schema_builder.QueryRoot,
                mutation=schema_builder.MutationRoot)

This change will cause DjraphQL to build additional operations for creating, updating, and deleting Songs.

Let’s pop open the Django shell again.

schema.execute('mutation { insertSong(data: { albumId: 3, title: "Secret Hidden Track", durationSeconds: 1 }) { success result { id } } }').data
# OrderedDict([('insertSong', {'success': True, 'result': {'id': 37}})])

schema.execute('query { SongsMany(where: { title: { _eq: "Secret Hidden Track" } }) { id } }').data
# {'SongsMany': [{'id': 37}]}

schema.execute('query { SongByPk(pk: 37) { title } }').data
# {'SongByPk': {'title': 'Secret Hidden Track'}}

schema.execute('mutation { updateSong(pk: 37 data: { title: "Secret Hidden Track #2", durationSeconds: 12 }) { success result { id } } }').data
# OrderedDict([('updateSong', {'success': True, 'result': {'id': 37}})])

schema.execute('query { SongByPk(pk: 37) { title } }').data
# {'SongByPk': {'title': 'Secret Hidden Track #2'}}

schema.execute('mutation { deleteSong(pk: 37) { success } }').data
# OrderedDict([('deleteSong', {'success': True})])

schema.execute('query { SongByPk(pk: 37) { title } }').errors
# [GraphQLLocatedError('Song matching query does not exist.')]

As you can see, we added a Song via insertSong, updated it via updateSong, and deleted it via deleteSong. All by adding a single line to our Entity.

Using GraphiQL

Now that we have a very basic GraphQL schema, we need to expose it as an API. We can use graphene-django to easily accomplish this.

More settings updates

First, we need to update our INSTALLED_APPS entry in settings.py again.

1
2
3
4
5
6
INSTALLED_APPS = [
    'songs.apps.SongsConfig',
    'graphene_django',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',

Next, add a /graphql endpoint to songster/urls.py:

1
2
3
4
5
6
7
8
from django.contrib import admin
from django.urls import path
from graphene_django.views import GraphQLView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('graphql', GraphQLView.as_view(graphiql=True)),
]

And one more change to settings.py. We must tell Graphene where to look for the schema object:

GRAPHENE = {
    "SCHEMA": "graphql_api.entities.schema"
}

Running our project

All our setup is done, let’s run our project!

python manage.py runserver

Now you should be able to navigate to http://127.0.0.1:8000/graphql and see the GraphiQL interface! Try running queries with the convenience of auto-complete.

One nice thing GraphiQL does is include the CSRF token automatically in requests it sends to the server.

But if we want to use something like curl to test our API, we’d have to either provide the CSRF token manually:

curl http://127.0.0.1:8000/graphql \
-H 'Cookie: csrftoken=1234mycsrftokenhere5678' \
-H 'X-CSRFToken: 1234mycsrftokenhere5678' \
-H 'Content-Type: application/json' \
-X POST \
--data-raw '{"query": "query GetAlbum($id: Int!) { AlbumByPk(pk: $id) { title } }", "variables": {"id": 1}}'

Or, we could use the csrf_exempt helper from the django.views.decorators.csrf module.

1
2
3
4
5
6
7
8
9
from django.urls import path
from graphene_django.views import GraphQLView
from django.views.decorators.csrf import csrf_exempt

urlpatterns = [
    path('admin/', admin.site.urls),
    path('graphql',
         csrf_exempt(GraphQLView.as_view(graphiql=True))),
]

With this change, we wouldn’t have to pass the CSRF token along with the curl request. This is certainly more convenient, but don’t do this in production!