Building a GraphQL API with Django
Building a GraphQL API with Django
Introduction
Web APIs are the engines that power most of our applications today. For many years REST has
been the dominant architecture for APIs, but in this article we will explore GraphQL.
With REST APIs, you generally create URLs for every object of data that's accessible. Let's say
we're building a REST API for movies - we'll have URLs for the movies themselves, actors,
awards, directors, producers... it's already getting unwieldy! This could mean a lot of requests for
one batch of related data. Imagine you were the user of a low powered mobile phone over a slow
internet connection, this situation isn't ideal.
GraphQL is not an API architecture like REST, it's a language that allows us to share related data
in a much easier fashion. We'll use it to design an API for movies. Afterwards, we'll look at how
the Graphene library enables us to build APIs in Python by making a movie API with Django.
What is GraphQL
Originally created by Facebook but now developed under the GraphQL Foundation, GraphQL is
a query language and server runtime that allows us to retrieve and manipulate data.
We leverage GraphQL's strongly-typed system to define the data we want available to the API.
We then create a schema for the API - the set of allowed queries to retrieve and alter data.
Types describe the kind of data that's available in the API. There are already provided primitive
types that we can use, but we can also define our own custom types.
type Actor {
id: ID!
name: String!
}
type Movie {
id: ID!
title: String!
actors: [Actor]
year: Int!
}
The ID type tells us that the field is the unique identifier for that type of data. If the ID is not a
string, the type needs a way to be serialized into a string to work!
Note: The exclamation mark signifies that the field is required.
You would also notice that in Movie we use both primitive types like String and Int as well as
our custom Actor type.
If we want a field to contain the list of the type, we enclose it in square brackets - [Actor].
Creating Queries
A query specifies what data can be retrieved and what's required to get to it:
type Query {
actor(id: ID!): Actor
movie(id: ID!): Movie
actors: [Actor]
movies: [Movie]
}
This Query type allows us to get the Actor and Movie data by providing their IDs, or we can get
a list of them without filtering.
Creating Mutations
A mutation describes what operations can be done to change data on the server.
Inputs - special types only used as arguments in a mutation when we want to pass an entire
object instead of individual fields.
Payloads - regular types, but by convention we use them as outputs for a mutation so we can
easily extend them as the API evolves.
input ActorInput {
id: ID
name: String!
}
input MovieInput {
id: ID
title: String
actors: [ActorInput]
year: Int
}
type ActorPayload {
ok: Boolean
actor: Actor
}
type MoviePayload {
ok: Boolean
movie: Movie
}
Take note of the ok field, it's common for payload types to include metadata like a status or an
error field.
type Mutation {
createActor(input: ActorInput) : ActorPayload
createMovie(input: MovieInput) : MoviePayload
updateActor(id: ID!, input: ActorInput) : ActorPayload
updateMovie(id: ID!, input: MovieInput) : MoviePayload
}
The createActor mutator needs an ActorInput object, which requires the name of the actor.
The updateActor mutator requires the ID of the actor being updated as well as the updated
information.
Note: While the ActorPayload and MoviePayload are not necessary for a successful mutation,
it's good practice for APIs to provide feedback when it processes an action.
Finally, we map the queries and mutations we've created to the schema:
schema {
query: Query
mutation: Mutation
}
GraphQL is platform agnostic, one can create a GraphQL server with a variety of programming
languages (Java, PHP, Go), frameworks (Node.js, Symfony, Rails) or platforms like Apollo.
With Graphene, we do not have to use GraphQL's syntax to create a schema, we only use
Python! This open source library has also been integrated with Django so that we can create
schemas by referencing our application's models.
Application Setup
Virtual Environments
It's considered best practice to create virtual environments for Django projects. Since Python 3.6,
the venv module has been included to create and manage virtual environments.
Using the terminal, enter your workspace and create the following folder:
$ mkdir django_graphql_movies
$ cd django_graphql_movies/
You should see a new env folder in your directory. We need to activate our virtual environment,
so that when we install Python packages they would only be available for this project and not the
entire system:
$ . env/bin/activate
Note: To leave the virtual environment and use your regular shell, type deactivate. You should
do this at the end of the tutorial.
While in our virtual environment, we use pip to install Django and the Graphene library:
A Django project can consist of many apps. Apps are reusable components within a project, and
it is best practice to create our project with them. Let's create an app for our movies:
$ cd django_graphql_movies/
$ django-admin.py startapp movies
Before we work on our application or run it, we'll sync our databases:
Django models describe the layout of our project's database. Each model is a Python class that's
usually mapped to a database table. The class properties are mapped to the database's columns.
class Actor(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Meta:
ordering = ('name',)
class Movie(models.Model):
title = models.CharField(max_length=100)
actors = models.ManyToManyField(Actor)
year = models.IntegerField()
def __str__(self):
return self.title
class Meta:
ordering = ('title',)
As with the GraphQL schema, the Actor model has a name whereas the Movie model has a title,
a many-to-many relationship with the actors and a year. The IDs are automatically generated for
us by Django.
We can now register our movies app within the project. Go the
django_graphql_movies/settings.py and change the INSTALLED_APPS to the following:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_graphql_movies.movies',
]
Be sure to migrate your database to keep it in sync with our code changes:
After we build our API, we'll want to be able to perform queries to test if it works. Let's load
some data into our database, save the following JSON as movies.json in your project's root
directory:
[
{
"model": "movies.actor",
"pk": 1,
"fields": {
"name": "Michael B. Jordan"
}
},
{
"model": "movies.actor",
"pk": 2,
"fields": {
"name": "Sylvester Stallone"
}
},
{
"model": "movies.movie",
"pk": 1,
"fields": {
"title": "Creed",
"actors": [1, 2],
"year": "2015"
}
}
]
Making Queries
In our movies app folder, create a new schema.py file and let's define our GraphQL types:
import graphene
from graphene_django.types import DjangoObjectType, ObjectType
from django_graphql_movies.movies.models import Actor, Movie
With Graphene's help, to create a GraphQL type we simply specify which Django model has the
properties we want in the API.
In the same file, add the following code to create the Query type:
if id is not None:
return Actor.objects.get(pk=id)
return None
if id is not None:
return Movie.objects.get(pk=id)
return None
The actor and movie properties return one value of ActorType and MovieType
respectively, and both require an ID that's an integer.
The actors and movies properties return a list of their respective types.
The four methods we created in the Query class are called resolvers. Resolvers connect the
queries in the schema to actual actions done by the database. As is standard in Django, we
interact with our database via models.
Consider the resolve_actor function. We retrieve the ID from the query parameters and return
the actor from our database with that ID as its primary key. The resolve_actors function
simply gets all the actors in the database and returns them as a list.
Making Mutations
When we designed the schema we first created special input types for our mutations. Let's do the
same with Graphene, add this to schema.py:
class MovieInput(graphene.InputObjectType):
id = graphene.ID()
title = graphene.String()
actors = graphene.List(ActorInput)
year = graphene.Int()
They are simple classes that define what fields can be used to change data in the API.
Creating mutations require a bit more work than creating queries. Let's add the mutations for
actors:
ok = graphene.Boolean()
actor = graphene.Field(ActorType)
@staticmethod
def mutate(root, info, input=None):
ok = True
actor_instance = Actor(name=input.name)
actor_instance.save()
return CreateActor(ok=ok, actor=actor_instance)
class UpdateActor(graphene.Mutation):
class Arguments:
id = graphene.Int(required=True)
input = ActorInput(required=True)
ok = graphene.Boolean()
actor = graphene.Field(ActorType)
@staticmethod
def mutate(root, info, id, input=None):
ok = False
actor_instance = Actor.objects.get(pk=id)
if actor_instance:
ok = True
actor_instance.name = input.name
actor_instance.save()
return UpdateActor(ok=ok, actor=actor_instance)
return UpdateActor(ok=ok, actor=None)
Recall the signature for the createActor mutation when we designed our schema:
The key thing to know when writing a mutation method is that you are saving the data on the
Django model:
We grab the name from the input object and create a new Actor object.
We call the save function so that our database is updated, and return the payload to the user.
The UpdateActor class has a similar setup with additional logic to retrieve the actor that's being
updated, and change its properties before saving.
ok = graphene.Boolean()
movie = graphene.Field(MovieType)
@staticmethod
def mutate(root, info, input=None):
ok = True
actors = []
for actor_input in input.actors:
actor = Actor.objects.get(pk=actor_input.id)
if actor is None:
return CreateMovie(ok=False, movie=None)
actors.append(actor)
movie_instance = Movie(
title=input.title,
year=input.year
)
movie_instance.save()
movie_instance.actors.set(actors)
return CreateMovie(ok=ok, movie=movie_instance)
class UpdateMovie(graphene.Mutation):
class Arguments:
id = graphene.Int(required=True)
input = MovieInput(required=True)
ok = graphene.Boolean()
movie = graphene.Field(MovieType)
@staticmethod
def mutate(root, info, id, input=None):
ok = False
movie_instance = Movie.objects.get(pk=id)
if movie_instance:
ok = True
actors = []
for actor_input in input.actors:
actor = Actor.objects.get(pk=actor_input.id)
if actor is None:
return UpdateMovie(ok=False, movie=None)
actors.append(actor)
movie_instance.title=input.title
movie_instance.year=input.year
movie_instance.save()
movie_instance.actors.set(actors)
return UpdateMovie(ok=ok, movie=movie_instance)
return UpdateMovie(ok=ok, movie=None)
As movies reference actors, we have to retrieve the actor data from the database before saving.
The for loop first verifies that the actors provided by the user are indeed in the database, if not it
returns without saving any data.
When working with many-to-many relationships in Django, we can only save related data after
our object is saved.
That's why we save our movie with movie_instance.save() before setting the actors to it with
movie_instance.actors.set(actors).
class Mutation(graphene.ObjectType):
create_actor = CreateActor.Field()
update_actor = UpdateActor.Field()
create_movie = CreateMovie.Field()
update_movie = UpdateMovie.Field()
As before when we designed our schema, we map the queries and mutations to our application's
API. Add this to the end of schema.py:
For our API to work, we need to make a schema available project wide.
import graphene
import django_graphql_movies.movies.schema
class Mutation(django_graphql_movies.movies.schema.Mutation,
graphene.ObjectType):
# This class will inherit from multiple Queries
# as we begin to add more apps to our project
pass
From here we can register graphene and tell it to use our schema.
In the same file, add the following code a couple of new lines below the INSTALLED_APPS:
GRAPHENE = {
'SCHEMA': 'django_graphql_movies.schema.schema'
}
GraphQL APIs are reached via one endpoint, /graphql. We need to register that route, or rather
view, in Django.
urlpatterns = [
path('admin/', admin.site.urls),
path('graphql/', GraphQLView.as_view(graphiql=True)),
]
To test our API, let's run the project and then go to the GraphQL endpoint. In the terminal type:
$ python manage.py runserver
Writing Queries
For our first query, let's get all actors in our database. In the top-left pane enter the following:
query getActors {
actors {
id
name
}
}
This is the format for a query in GraphQL. We begin with the query keyword, followed by an
optional name for the query. It's good practice to give queries a name as it helps with logging and
debugging. GraphQL allows us to specify the fields we want as well - we chose id and name.
Even though we only have one movie in our test data, let's try the movie query and discover
another great feature of GraphQL:
query getMovie {
movie(id: 1) {
id
title
actors {
id
name
}
}
}
The movie query requires an ID, so we provide one in brackets. The interesting bit comes with
the actors field. In our Django model we included the actors property in our Movie class and
specified a many-to-many relationship between them. This allows us to retrieve all the properties
of an Actor type that's related to the movie data.
This graph-like traversal of data is a major reason why GraphQL is considered to be powerful
and exciting technology!
Writing Mutations
Mutations follow a similar style as queries. Let's add an actor to our database:
mutation createActor {
createActor(input: {
name: "Tom Hanks"
}) {
ok
actor {
id
name
}
}
}
Notice how the input parameter corresponds to the input properties of the Arguments classes
we created earlier.
Also note how the ok and actor return values map to the class properties of the CreateActor
mutation.
mutation createMovie {
createMovie(input: {
title: "Cast Away",
actors: [
{
id: 3
}
]
year: 1999
}) {
ok
movie{
id
title
actors {
id
name
}
year
}
}
}
Unfortunately, we just made a mistake. "Cast Away" came out in the year 2000!
mutation updateMovie {
updateMovie(id: 2, input: {
title: "Cast Away",
actors: [
{
id: 3
}
]
year: 2000
}) {
ok
movie{
id
title
actors {
id
name
}
year
}
}
}
GraphiQL is very useful during development, but it's standard practice to disable that view in
production as it may allow an external developer too much insight into the API.
An application communicating with your API would send POST requests to the /graphql
endpoint. Before we can make POST requests from outside the Django site, we need to change
django_graphql_movies/urls.py:
urlpatterns = [
path('admin/', admin.site.urls),
path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
]
Django comes built-in with CSRF (Cross-Site Request Forgery) protection - it has measures to
prevent incorrectly authenticated users of the site from performing potentially malicious actions.
While this is useful protection, it would prevent external applications from communicating with
the API. You should consider other forms of authentication if you put your application in
production.
$ curl \
-X POST \
-H "Content-Type: application/json" \
--data '{ "query": "{ actors { name } }" }' \
http://127.0.0.1:8000/graphql/
Conclusion
GraphQL is a strongly-typed query language that helps to create evolvable APIs. We designed an
API schema for movies, creating the necessary types, queries and mutations needed to get and
change data.
With Graphene we can use Django to create GraphQL APIs. We implemented the movie schema
we designed earlier and tested it using GraphQL queries via GraphiQL and a standard POST
request.
If you'd like to see the source code of the complete application, you can find it here.