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 Song
s.
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!