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.

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 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.

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:

  • 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:

# 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 require 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 the GraphQL's query name.
  • The inner Arguments class properties correspond to the input arguments for the mutator.
  • The ok and actor properties make up the ActorPayload.

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.yearce.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.

Author image
Trinidad and Tobago Twitter Website
Web Dev|Games|Music|Art|Fun|Caribbean I love many things and coding is one of them!