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.
Designing a Movie Schema
Creating Our Types
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.
Consider the following types for actors and movies:
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 ID
s, 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.
Mutations rely on two things:
- 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.
The first thing we do is create the input types:
input ActorInput {
id: ID
name: String!
}
input MovieInput {
id: ID
title: String
actors: [ActorInput]
year: Int
}
And then we create the payload types:
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.
The Mutation
type brings it all together:
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.
The same follows for the movie mutators.
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.
Defining the Schema
Finally, we map the queries and mutations we've created to the schema:
schema {
query: Query
mutation: Mutation
}
Using the Graphene Library
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/
Now create the virtual environment:
$ python3 -m venv env
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.
Installing and Configuring Django and Graphene
While in our virtual environment, we use pip
to install Django and the Graphene library:
$ pip install Django
$ pip install graphene_django
Then we create our Django project:
$ django-admin.py startproject django_graphql_movies .
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:
# First return to the project's directory
$ cd ..
# And then run the migrate command
$ python manage.py migrate
Creating a Model
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.
Type the following code to django_graphql_movies/movies/models.py
:
from django.db import models
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:
$ python manage.py makemigrations
$ python manage.py migrate
Loading Test Data
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"
}
}
]
And run the following command to load the test data:
$ python manage.py loaddata movies.json
You should see the following output in the terminal:
Installed 3 object(s) from 1 fixture(s)
Creating our Schema with Graphene
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
# Create a GraphQL type for the actor model
class ActorType(DjangoObjectType):
class Meta:
model = Actor
# Create a GraphQL type for the movie model
class MovieType(DjangoObjectType):
class Meta:
model = 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:
# Create a Query type
class Query(ObjectType):
actor = graphene.Field(ActorType, id=graphene.Int())
movie = graphene.Field(MovieType, id=graphene.Int())
actors = graphene.List(ActorType)
movies= graphene.List(MovieType)
def resolve_actor(self, info, **kwargs):
id = kwargs.get('id')
if id is not None:
return Actor.objects.get(pk=id)
return None
def resolve_movie(self, info, **kwargs):
id = kwargs.get('id')
if id is not None:
return Movie.objects.get(pk=id)
return None
def resolve_actors(self, info, **kwargs):
return Actor.objects.all()
def resolve_movies(self, info, **kwargs):
return Movie.objects.all()
Each property of the Query
class corresponds to a GraphQL query:
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
-
The
actor
andmovie
properties return one value ofActorType
andMovieType
respectively, and both require an ID that's an integer. -
The
actors
andmovies
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
:
# Create Input Object Types
class ActorInput(graphene.InputObjectType):
id = graphene.ID()
name = graphene.String()
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 requires a bit more work than creating queries. Let's add the mutations for actors:
# Create mutations for actors
class CreateActor(graphene.Mutation):
class Arguments:
input = ActorInput(required=True)
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:
createActor(input: ActorInput) : ActorPayload
- Our class' name corresponds to GraphQL's query name.
- The inner
Arguments
class properties correspond to the input arguments for the mutator. - The
ok
andactor
properties make up theActorPayload
.
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.
Now let's add the mutation for movies:
# Create mutations for movies
class CreateMovie(graphene.Mutation):
class Arguments:
input = MovieInput(required=True)
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)
.
To complete our mutations, we create the Mutation type:
class Mutation(graphene.ObjectType):
create_actor = CreateActor.Field()
update_actor = UpdateActor.Field()
create_movie = CreateMovie.Field()
update_movie = UpdateMovie.Field()
Making the Schema
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
:
schema = graphene.Schema(query=Query, mutation=Mutation)
Registering the Schema in the Project
For our API to work, we need to make a schema available project wide.
Create a new schema.py
file in django_graphql_movies/
and add the following:
import graphene
import django_graphql_movies.movies.schema
class Query(django_graphql_movies.movies.schema.Query, graphene.ObjectType):
# This class will inherit from multiple Queries
# as we begin to add more apps to our project
pass
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
schema = graphene.Schema(query=Query, mutation=Mutation)
From here we can register graphene and tell it to use our schema.
Open django_graphql_movies/settings.py
and add 'graphene_django',
as the first item in the INSTALLED_APPS
.
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.
Open django_graphql_movies/urls.py
and change the file contents to:
from django.contrib import admin
from django.urls import path
from graphene_django.views import GraphQLView
from django_graphql_movies.schema import schema
urlpatterns = [
path('admin/', admin.site.urls),
path('graphql/', GraphQLView.as_view(graphiql=True)),
]
Testing Our API
To test our API, let's run the project and then go to the GraphQL endpoint. In the terminal type:
$ python manage.py runserver
Once your server is running head to http://127.0.0.1:8000/graphql/
. You'll encounter GraphiQL - a built in IDE to run your queries!
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.
Now we can add a movie that Tom Hanks acted in:
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!
Let's run an update query to fix it:
mutation updateMovie {
updateMovie(id: 2, input: {
title: "Cast Away",
actors: [
{
id: 3
}
]
year: 2000
}) {
ok
movie{
id
title
actors {
id
name
}
year
}
}
}
There, all fixed!
Communicating via POST
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.
To disable GraphiQL, simply edit django_graphql_movies/urls.py
such that path('graphql/', GraphQLView.as_view(graphiql=True)),
becomes path('graphql/', GraphQLView.as_view(graphiql=False)),
.
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
:
from django.contrib import admin
from django.urls import path
from graphene_django.views import GraphQLView
from django_graphql_movies.schema import schema
from django.views.decorators.csrf import csrf_exempt # New library
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.
In your terminal enter the following to get all actors:
$ curl \
-X POST \
-H "Content-Type: application/json" \
--data '{ "query": "{ actors { name } }" }' \
http://127.0.0.1:8000/graphql/
You should receive:
{"data":{"actors":[{"name":"Michael B. Jordan"},{"name":"Sylvester Stallone"},{"name":"Tom Hanks"}]}}
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.