How to get started with GraphQL on Django

Published by moxlotus on

ok so I suppose that you are here because you have not been able to find enough resources out there regarding the usage of GraphQL with Django. Congratulation, you have come to the right place. After slogging through the internet to find all the information that is needed, I have decided to put up an article regarding some basic usage of GraphQL, mainly CRUD operations, with Django.

So lets get started from the scratch with a new django project.
First thing, install django and graphene for django.
graphene_django is a python package that allows django to act as a GraphQL Server. And django-filter is needed for the example to handles filters in query, you may not need this in your own project.

pip install django
pip install graphene_django
pip install django-filter 

If you have virtualenv/virtualenvwrapper installed, feel free to use it before running this.

Next lets create a project folder for django

mkdir GraphQLExample
cd GraphQLExample

Create the django project and django app.

django-admin.py startproject catalog
cd catalog
django-admin.py startapp books
python3 manage.py migrate

Please check and make sure that your project folder structure looks like this:

GraphQLExample
|_catalog
  |_catalog
  |_books
  |_manage.py  

next, we will create some django models in books/models.py

from django.db import models

# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=100)

def __str__(self):
return self.name

class Book(models.Model):
name = models.CharField(max_length=100)
author = models.CharField(max_length=100)
category = models.ForeignKey(Category, related_name='books')

def __str__(self):
return self.name

The following part is where we will set up our GraphQL backend. create a schema.py in books
schema.py is synonymous to urls.py in a django project. In this file, we will define how our queries will be handled by the GraphQL backend. we will start with simple queries like list and retrieve. Later in the article, I will touch on how to use mutation to do create/update/delete operations.

from graphene import AbstractType
from graphene import Field
from graphene import Node
from graphene import ClientIDMutation
from graphene import String
from graphene import Float

from graphene_django.filter import DjangoFilterConnectionField
from graphene_django.types import DjangoObjectType

from .models import Category
from .models import Book

class CategoryNode(DjangoObjectType):
    class Meta:
        model = Category
        interfaces = (Node, )
        filter_fields = ['name', 'books']
        filter_order_by = ('name')


class BookNode(DjangoObjectType):
    class Meta:
        model = Book
        interfaces = (Node, )
        filter_fields = {
            'name': ['exact', 'icontains', 'istartswith'],
            'author': ['exact', 'icontains'],
            'category': ['exact'],
            'category__name': ['exact'],
        }
        filter_order_by = ('name', 'category__name')

class CategoryQuery(AbstractType):
     category = DjangoFilterConnectionField(CategoryNode)
 
class BookQuery(AbstractType):
     book = Node.Field(BookNode)
     all_books = DjangoFilterConnectionField(BookNode)

Let me briefly explain what is actually happening here. In the schema.py found in the app, we have defined 2 Node classes, BookNode and CategoryNode. If you are familiar with django RESTful framework, they are pretty similar to serializer. In the Meta class of BookNode and CategoryNode, you can see that several options has been indicated to define the behaviour that is supported when queried. There are additional options like exclude_field and only_field. You may dig through the source code of graphene for more information.

After defining the Node Classes, we will need to consolidate them into a Query class that is of type AbstractType. The reason why this is AbstractType is to allow inheritance later by the Root Query class. the snake_case in the BookQuery and CategoryQuery will be transformed into camelCase. So in your query, you will refer to allBooks instead of all_books.

Next, we will create a root schema in the catalog folder. catalog/schema.py

import graphene
from books.schema import BookQuery
from books.schema import CategoryQuery

class RootQuery(BookQuery
        , CategoryQuery
        , graphene.ObjectType):
    pass

schema = graphene.Schema(query=RootQuery)

Next we will need to enable GraphQL in our project. This is achieved by changing the django project settings.
Open catalog/settings.py and add the following to the file.


INSTALLED_APPS = (
   ...
   'books',
   'graphene_django',
)

GRAPHENE = {
    'SCHEMA' : 'catalog.schema.schema', #points to the schema variable in schema.py
    'SCHEMA_INDENT': 2, #defines the indentation space in the output
}

Now we need to add an url in the catalog/urls.py so that our queries can be received by django backend.

from django.conf.urls import include, url
from django.contrib import admin
from graphene_django.views import GraphQLView

urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^api/v1/', GraphQLView.as_view(graphiql=True)),
]

Time to do django migrations to initialize the database


python3 manage.py migrate
python3 manage.py makemigrations books
python3 manage.py migrate books

Run the server and open localhost:8000/api/v1/

python3 manage.py runserver

You will be able to see the interactive query window in your browser
In the event that you are getting assertion error on "order_by", downgrade django-filter to 0.11.0.

Try the follow query in the browser

{
  allBooks{
    edges{
      node{
        id
        name
        author
      }
    }
  }
}

You should be seeing the following in your browser

{
  "data": {
    "allBooks": {
      "edges": []
    }
  }
}

As you can see, the dataset is currently empty. Instead of loading a json file to populate the database like what other GraphQL examples did, I am gonna show you how to write the mutation to create a new record.

Add the following code into your books/schema.py

class NewCategory(ClientIDMutation):
    category = Field(CategoryNode)
    class Input:
        name = String()

    @classmethod
    def mutate_and_get_payload(cls, input, context, info):
        temp = Category(
             name = input.get('name') ,
        )
        temp.save()
        return NewCategory(category=temp)

class NewBook(ClientIDMutation):
    book = Field(BookNode)
    class Input:
        name = String()
        author = String()
        category = String()
    
    @classmethod
    def mutate_and_get_payload(cls, input, context, info):
        book = Book(
            name = input.get('name'),
            author = input.get('author'),
            category = Category.objects.get(name=input.get('category'))            
        )
        book.save()
        return NewBook(book=book)

class CategoryMutation(AbstractType):
    new_category = NewCategory.Field()

class BookMutation(AbstractType):
    new_Book = NewBook.Field()

Now we need to create a RootMutation in the Root schema. In the catalog/schema.py
Changes are highlighted in bold.

import graphene
from books.schema import BookQuery
from books.schema import CategoryQuery
from books.schema import BookMutation
from books.schema import CategoryMutation

class RootQuery(BookQuery
        , CategoryQuery
        , graphene.ObjectType):
    pass

class RootMutation(BookMutation
        , CategoryMutation
        , graphene.ObjectType):
    pass

schema = graphene.Schema(query=RootQuery, mutation=RootMutation)

As you can see that it is very similar to how Query is handled.
Run the following queries in the browser to add 2 categories and 2 books

mutation{
  newCategory(input:{name:"Fiction"}){
    category{
      name
    }
  }
}
mutation{
  newCategory(input:{name:"Sci-fi"}){
    category{
      name
    }
  }
}

mutation{
  newBook(input:{name:"Harry Potter", author:"JK Rowing", category:"Fiction"}){
    book{
      name
      author
      category{
        name
      }
    }
  }
}
mutation{
  newBook(input:{name:"Starwars", author:"George Lucas", category:"Sci-fi"}){
    book{
      name
      author
      category{
        name
      }
    }
  }
}

Let's take a look at the data that has been inserted so far


{
  allBooks{
    edges{
      node{
        name
        author
        category{
          name
        }
      }
    }
  }
}

The output that you should be seeing in your browser:

{
  "data": {
    "allBooks": {
      "edges": [
        {
          "node": {
            "name": "Harry Potter",
            "author": "JK Rowing",
            "category": {
              "name": "Fiction"
            }
          }
        },
        {
          "node": {
            "name": "Starwars",
            "author": "George Lucas",
            "category": {
              "name": "Sci-fi"
            }
          }
        }
      ]
    }
  }
}

Now that we have learnt how to create a new record in our database, lets try to update the record.
We will now need to create a new mutation method that allow us to update the book information
Add the following code into your book/schema.py

from graphql_relay.node.node import from_global_id

class UpdateBook(ClientIDMutation):
    book = Field(BookNode)
    class Input:
        id = String()
        name = String()
        author = Float()
    
    @classmethod
    def mutate_and_get_payload(cls, input, context, info):
        book = Book.objects.get(pk=from_global_id(input.get('id'))[1])
        book.name=input.get('name')
        book.author=input.get('author')
        book.save()
        return UpdateBook(book=book)

class BookMutation(AbstractType):
    new_Book = NewBook.Field()
    update_book = UpdateBook.Field()

Unlike Create, you will need the id of the book in order to update it. However the id given by GraphQL is different from the id in django model. Fortunately, there is a way to convert the GraphQL id to Django model id by using from_global_id.

Find the id of the book using the allBooks method.


{
  allBooks{
    edges{
      node{
        id
        name
        author
      }
    }
  }
}

The output in my browser shows:

{
  "data": {
    "allBooks": {
      "edges": [
        {
          "node": {
            "id": "Qm9va05vZGU6MQ==",
            "name": "Starwars",
            "author": "George Lucas"
          }
        },
        {
          "node": {
            "id": "Qm9va05vZGU6Mg==",
            "name": "Starwars: new hope",
            "author": "George Lucas"
          }
        }
      ]
    }
  }
}

I am going to update the book with id = "Qm9va05vZGU6Mg==". The one that you see might be different, so please change accordingly.

mutation{
  updateBook(input:{id:"Qm9va05vZGU6Mg==", name:"Starwars: new hope", author:"George Lucas"}){
    book{
      name
      author
      category{
        name
      }
    }
  }
}

To delete the object, is very similar to update and create. So I will leave that to you to explore.
I hope that after reading this you will be able to start using GraphQL in your django project. =)

You may find all the code in my github repository here

Share it with others