If you're here, you know what GraphQL is. In this tutorial, you'll see how you can use it with Django to set up a single endpoint for all your data, using graphene and graphene_django.

The code written in this tutorial can all be found here, if you wanna take a look.

Prerequisites

To follow this tutorial, you will need:

  • A basic understanding of Python
  • Basic Django knowledge

Before we start, I know it's frustrating to see me set up the project, because that's not what you came here for. You can skip Step 0 if you want, but I would still recommend reading it.

Step 0 - Setting up the project

Initial setup

First, let's create the project directory and create a virtual environment inside it. I'll call this project task_manager.

$ mkdir task-manager && cd task-manager
$ virtualenv venv -p python3

Activate the environment and install Django.

$ source venv/bin/activate
$ pip install django

Inside the task-manager directory, initialize a new Django project called  task_manager and create a new app called main. You will use this app for the GraphQL API.

$ django-admin startproject task_manager
$ cd task_manager
$ python manage.py startapp main

Add the main app to the INSTALLED_APPS of your project.

 ...

INSTALLED_APPS = [
 'main.apps.MainConfig',
 
 ...

]

 ...
task_manager/settings.py

Creating the models

We'll create a schema in which every user can be assigned multiple tasks, but a single task can only have one assignee. It's basically a one-to-many relationship from user to tasks.

Open up main/models.py and create a Task model to store the details of a task.

from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone


class Task(models.Model):
    assigned_to = models.ForeignKey(User, related_name="tasks", on_delete=models.CASCADE)
    title = models.CharField(max_length = 50)
    description = models.TextField()
    status = models.BooleanField(default=False)
    created_on = models.DateTimeField(default=timezone.now)
    

    def __str__(self):
        return self.title
main/models.py

Create the migrations and start up the server to make sure everything went well.

$ python manage.py makemigrations main
$ python manage.py migrate
$ python manage.py runserver

Let's create some mock data from the django shell to test our queries. Start the shell with this command.

$ python manage.py shell

Make a few users and a few tasks, for instance -

>> from django.contrib.auth.models import User
>> from main.models import Task

>> User.objects.create_user(username='TestUser1', password='test123')

>> User.objects.create_user(username='TestUser2', password='test123')

>> Task.objects.create(assigned_to_id=1, title='Eat', description='Finish all the greens.')

>> Task.objects.create(assigned_to_id=1, title='Assign', description='Monitor, evaluate and employ')

>> Task.objects.create(assigned_to_id=2, title='Learn', description='If you wish to.')

Step 1 - Configuring the dependencies

We'll be using graphene and graphene_django to implement the GraphQL endpoint.

Why Graphene?                                                                  

Graphene lets you define your GraphQL schema using application code. This means you do not have to define a separate GraphQL schema and you don't need to explicitly make schema changes whenever your models change. Pretty convenient (though it has it's cons).

Setting up dependencies

Install graphene and graphene_django.

$ pip install graphene graphene_django

Add graphene_django to installed apps of your project, in task_manager/settings.py

 ...
 
 INSTALLED_APPS = [
  'graphene_django',
  
  ...
 ]
  ...
task_manager/settings.py

Step 2 - Writing your first Model Type in GraphQL

Begin by creating a directory main/gql/.

# Inside task-manager/main
mkdir gql

This directory will contain all the code related to our GraphQL implementation.

The code we write here cannot directly interact with the Django models we earlier defined. So, create a new file inside the gql directory, called typeDef.py. This file will contain model types that act as a layer of abstraction between our Django models and our GraphQL implementation.

Let's begin by making a GraphQL Type for the Task model. Add the following code to a file named main/gql/typeDef.py

from graphene_django.types import DjangoObjectType
from main.models import Task

class TaskType(DjangoObjectType):
    class Meta:
        model = Task
        fields = ('id', 'assigned_to', 'title', 'description', 'status', 'created_on')
main/gql/typeDef.py

DjangoObjectType maps the model fields to appropriate GraphQL Inside the fields attribute of the Meta class, you can specify all the fields on the model that you want to access through GraphQL.

By default, if the fields are not specified, Graphene exposes all fields of the model. The documentation strongly recommends that you explicitly declare thee fields you want to expose as this prevents unintentional exposure of data.

Step 3 - Writing your first GraphQL Root Type

To enable the client to interact with the model type you need to define root types. GraphQL has 3 root types - queries, mutations and subscriptions. In this tutorial, we will cover only subscriptions.

Defining the queries and resolvers

Queries are used whenever you wish to fetch data from the server. Create a new file called query.py in the gql directory. We will define all our queries in this file.

Inside query.py, add this code.

import graphene
from .typeDef import TaskType
from main.models import Task

class Query:
    all_tasks = graphene.List(TaskType)
  
    def resolve_all_tasks(self, info, **kwargs):
  	    return Task.objects.all()
main/gql/query.py

graphene abstracts the GraphQL types (called scalars) which can be used to define the type of fields. In this case, to tell graphene that the all_tasks field should be a list of tasks, we use graphene.List(TaskType).

The method resolve_all_tasks actually performs the query and populates the all_tasks field. Functions like this are called resolvers. You'll need to define resolvers for all the queries you define.

Resolver functions are method of Query class and follow the naming convention resolve_query_name. They have three arguments:

  1. self - This is the default parameter of every method in a python class.
  2. info - This has information about the context, user logged In, etc.
  3. **kwargs - Additional information that is send by the client.

Setting up the schema

Now that you're done defining your first query, you can make it accessible from the GraphQL endpoint. To do this, you'll need to add it to the project settings.

Create a file in the task_manager directory (the one with settings.py) called schema.py. In this file, add this code to load Queries from all the apps in our Django Project.

Why are we doing this?
When you have a larger project with multiple apps, having the whole GraphQL schema in a single app can be difficult to maintain. That's why, we define a seperate schema for each app combine each schema inside the project's schema. This way you can seperately develop the schema for each app.
import graphene
from main.gql.query import Query as MainQuery

class Query(MainQuery, graphene.ObjectType):
    pass

schema = graphene.Schema(query=Query)
task_manager/schema.py

Now load base schema in the project settings by adding this code in settings.py.

 ...

GRAPHENE = {
 'SCHEMA': 'task_manager.schema.schema',
}
 
 ...
task_manager/settings.py

Above, we specify that we want Graphene to use the schema that is located inside task_mager/schema.py.

In the root urls.py file, add the GraphQL endpoint.

  ...
  
 from django.views.decorators.csrf import csrf_exempt
 from graphene_django.views import GraphQLView
 
 urlpatterns =[
  ...
  path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True)))
  ...
 ]
 
task_manager/urls.py

We're using csrf_exempt here because we're just testing things out.

Now save all the changes you have made and run server.

$ python manage.py runserver

Step 4 - Using GraphiQL to make your first query

Visit 127.0.0.1:8000/graphql from your browser and you should see a GraphiQL playground which you will use to send request and get responses, it is a great tool as it automatically documents the project and tells about all the queries, mutations and subscriptions that the server supports.

If you click on the docs tab on the right-hand side you will find that there is a ROOT TYPES heading which lists query. If you click on it, it will list all the queries, presently you should have just one 'allTasks' which is specified as an array of 'TaskType'. If you click on this it lists all the fields and their types which can be demanded from the server like shown below.

Notice how your query is named allTasks even though you defined it as all_tasks. Graphene converts snake_case to snakeCase to meet the GraphQL standards.

If you take a closer look, you will notice that there is no assigned_to field in TaskType even-though it is present in models, this is because we have not yet defined UserType and that is the scalar of assigned_to field, I'll be coming back to this once we define UserType.

Try running this query now.

query {
  allTasks {
    title
    description
  }
}

As you can see, the server responds with only the title and description fields for each query.  

Step 5 - Adding more queries

Now that you know how to get a list of all tasks, lets see how you can get individual tasks by their ID.

Inside the Query class in query.py, add this code.

 ...
from django.core.exceptions import ObjectDoesNotExist
 
class Query:
 ...
    task = graphene.Field(TaskType, id=graphene.ID())
 
 ...
 
    def resolve_task(self, info, **kwargs):
        try:
            return Task.objets.get(pk=kwargs.get('id'))
        except ObjectDoesNotExist:
            return None
main/gql/query.py

Here, graphene.Field(TaskType, id=graphene.ID()) does the following two things

  1. The first argument i.e. TaskType tells graphene that the field task should resolve to a single TaskType instance (which just means that when you use the task query, you'll get back a single task).
  2. The second argument i.e. id=graphene.ID() tells graphene that the client is going to provide a variable called id of type graphene.ID() in the arguments. This id will can be accessed in the resolver for this field to fetch data accordingly. In this case, we will use this id to fetch a Task with the same id.

Inside our resolver, we use the id (which is available to us in the kwargs argument) to fetch the appropriate Task with that id. Querying the database with get throws an error in case a task with that ID does not exist so we wrap the query with a try-except block.

Try out this query. If all goes well, you should something like the result shown in the image below.

query {
	task (id:1) {
    	title
        status
    }
}
Note that the GraphQL server returns a status code of 200 OK even when it returns a error so make sure you check for errors field in data when working on the client-side of the project.

So now you can do the following -

  • Fetch all tasks
  • Fetch a task by id

You can similarly add more queries. You can pass any kind of variables in your queries as additional arguments (just like we passed id), so you can have pretty much any kind of query this way.

Step 6 - Writing Queries for Users

Let's go over all that again to recap what you learnt. This time, we'll add queries for the User model. We'll be using the default User Model that comes with Django for this.

Start by adding another type - UserType. In the typeDef.py file, add the following code.

 ...
from django.contrib.auth.models import User

 ...

class UserType(DjangoObjectType):
    class Meta:
        model = User
        fields = ('id', 'username', 'password', 'task')
main/gql/typeDef.py

Save the changes and run the server.

$ python manage.py runserver

Take a look at the fields available with TaskType it now has assignedTo field because we are past defining the UserType. How did graphene know which type assignedTo maps to? Well that's some django style magic right there.

Here's what  happened: In TaskType, you set model = Task. In the Task model, assigned_to is a foreign key to User. The User model is then used in UserType. So, django_graphql did all that detective work for you and set the assignedTo field to UserType.

The UserType serves as an abstraction layer over the default User Model. The default model comes with a whole set of fields but we will be interacting with only 4 of them so we have exposed only those fields in the GraphQL schema.

Now add a query for allUsers and userById.

 ...
from .typeDefs import UserType
from django.contrib.auth.models import User

class Query:
 ...
    all_users = graphene.List(UserType)
    user = graphene.Field(UserType, id=graphene.Int())
   
 ...
    def resolve_all_users(self, info, **kwargs):
    	return User.objects.all()
       
    def resolve_user(self, info, **kwargs):
    	try:
        	return User.objects.get(pk=kwargs.get('id'))
        except ObjectDoesNotExist:
        	return None
main/gql/query.py

If you save changes, run server and have a look at docs tabs it will show the 2 new queries.

Now, you can make queries any way you wish. For example, try running the following query -

allTasks {
  assignedTo {
    id
    task {
      title
    }
  }
}

This query will list all tasks, inside every task will be the user to whom the task is assigned. Furthermore, inside every user there will be all the tasks assigned to that user. This query is redundant, but it shows how powerful GraphQL is. We basically got users from tasks, and then got the tasks from users again. In technical speak, you can traverse your model-relationship graph in any way you want.

Conclusion

If you've managed to follow this through, you should be having a Task Manager App capable of reading tasks and users using GraphQL endpoint, at your deposition. In the next tutorial we'll see how to write about writing mutations that can be used to create, update and delete tasks and users.

Once your GraphQL API is ready, the client side developer can truly fetch any data in any desired format, in a single API call. That's powerful.

All criticism and questions are welcome. If any of the above doesn't work: file an issue at the repository, or leave a comment below.