GraphQL is an open-source data query and manipulation language for APIs, and a runtime for fulfilling queries with existing data. It was developed internally by Facebook in 2012 before being publicly released in 2015. It allows clients to define the structure of the data required, and the same structure of the data is returned from the server, therefore preventing unnecessary data from being returned.
GraphQL has three primary operations: Queries for reading data, Mutations for writing data, and Subscriptions for automatically receiving real-time data updates. A GraphQL server provides clients with a predefined schema – a model of the data that can be requested. The schema serves as common ground between the client and the server.
In this tutorial we will use Graphene, a GraphQL framework for Python, to build a Django API that uses queries and mutations.
Tutorial Requirements
To follow along with this tutorial you should have the following items:
- Python 3.6 or newer.
- Virtualenv, to create a virtual environment for the tutorial project.
- Postman, to send requests to our API.
- A working knowledge of the Django web framework.
Project setup
We will begin by creating a virtual environment and installing necessary Python packages.
Create a folder for our project:
mkdir django_graphql
cd django_graphql
Then create a Python virtual environment and activate it. If you are following this tutorial on Windows:
$ virtualenv env
$ env\Scripts\activate
If you are using a Mac OS or Unix computer:
$ virtualenv env
$ source env/bin/activate
Install the dependencies required for our project:
(env) $ pip install django graphene-django
Create a new django project:
(env) $ django-admin startproject books_api
Change your current directory to the project:
(env) $ cd books_api
Create a api
app in the books_api
project
(env) $ python manage.py startapp api
Next register the api
app and integrate the graphene-django
third-party app we installed earlier into our books_api
project. Find the INSTALLED_APPS
list In books_api/settings.py
and add api
and `graphene-django’ at the end:
INSTALLED_APPS = [
...
'api',
'graphene_django',
]
While in books_api/settings.py
, go to the bottom of the file and add a GRAPHENE
dictionary with settings for the graphene-django
package:
GRAPHENE = {
"SCHEMA": "api.schema.schema"
}
The SCHEMA
setting tells Graphene where to find the GraphQL schema for the application. We’ll define the schema after we have the database created.
Database model
Open the api/models.py
file and type in the code below to add the Book
database model:
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.CharField(max_length=50)
year_published = models.CharField(max_length=10)
review = models.PositiveIntegerField()
def __str__(self):
return self.title
Then create and run the migrations for our database:
$ python manage.py makemigrations
$ python manage.py migrate
To help in testing this project we can now populate our database with some data. To do this, you create a data.json
file in the project directory where the manage.py
file is, and copy the following data into it:
[
{
"model": "api.book",
"pk": 1,
"fields": {
"title": "The One Thing",
"author": "",
"year_published": 2002,
"review": 3
}
},
{
"model": "api.book",
"pk": 2,
"fields": {
"title": "Python Crash Course",
"author": "Eric Matthes",
"year_published": 2015,
"review": 5
}
},
{
"model": "api.book",
"pk": 3,
"fields": {
"title": "Atomic Habits",
"author": "James Clear",
"year_published": 2002,
"review": 4
}
},
{
"model": "api.book",
"pk": 4,
"fields": {
"title": "The Compound Effect",
"author": "Darren Hardy",
"year_published": 2010,
"review": 3
}
},
{
"model": "api.book",
"pk": 5,
"fields": {
"title": "Clean Code",
"author": "Robert Cecil Martin",
"year_published": 2008,
"review": 4
}
}
]
With the data.json
file saved to the current directory, run the command below to import the data into the database:
$ python manage.py loaddata data.json
Installed 5 object(s) from 1 fixture(s)
Next, add the GraphQL endpoint at the end of the urlpatterns
dictionary in file books_api/urls.py
:
from django.contrib import admin
from django.urls import path
from graphene_django.views import GraphQLView
urlpatterns = [
path('admin/', admin.site.urls),
path('graphql', GraphQLView.as_view(graphiql=True)),
]
Building a Books API with GraphQL
In this section we will be building an API with Graphene using GraphQL queries and mutations.
Implementing a GraphQL Schema
Create a new file in the api/schema.py
folder:
import graphene
from graphene_django import DjangoObjectType, DjangoListField
from .models import Book
class BookType(DjangoObjectType):
class Meta:
model = Book
fields = "__all__"
class Query(graphene.ObjectType):
all_books = graphene.List(BookType)
book = graphene.Field(BookType, book_id=graphene.Int())
def resolve_all_books(self, info, **kwargs):
return Book.objects.all()
def resolve_book(self, info, book_id):
return Book.objects.get(pk=book_id)
In this step we have created two classes, The first one is BookType
, which adapts the Book
model to a DjangoObjectType
. We set fields
to __all__
to indicate that we want all the fields in the model available in our API.
The Query
class defines the GraphQL queries that the API will provide to clients. The all_books
query will return a list of all the BookType
instances, while the book
query will return one BookType
instance, given by an integer ID. The class defines two methods, which are the query “resolvers”. Every query in the schema maps to a resolver method.
The two query resolvers query the database using the Django model to execute the query and return the results.
Adding data updates with GraphQL mutations
We will now add create, update and delete operations through mutations. While still in the api/schema.py
file, add the code below at the bottom:
class BookInput(graphene.InputObjectType):
id = graphene.ID()
title = graphene.String()
author = graphene.String()
year_published = graphene.String()
review = graphene.Int()
The BookInput
class defines fields similar to our Book model object to allow the client to add or change the data through the API. We will use this class as an argument for our mutation classes.
Let’s add a mutation to create new books. Add the following code at the bottom of api/schema.py
:
class CreateBook(graphene.Mutation):
class Arguments:
book_data = BookInput(required=True)
book = graphene.Field(BookType)
@staticmethod
def mutate(root, info, book_data=None):
book_instance = Book(
title=book_data.title,
author=book_data.author,
year_published=book_data.year_published,
review=book_data.review
)
book_instance.save()
return CreateBook(book=book_instance)
The CreateBook
class will be used to create and save new Book
entries to the database. For every mutation class we must have an Arguments
inner class and a mutate()
class method.
We defined an instance of the BookInput
class we created earlier as our arguments, and we made it mandatory with the required=True
option. After that we defined the model we are working with by doing this book = graphene.Field(BookType)
.
In the mutate
method we are saving a new book by calling the save()
method on a new Book
instance created from the book_data
values passed as argument.
Below you can see the implementation of the UpdateBook
mutation. Add this code at the bottom of api/schema.py
:
class UpdateBook(graphene.Mutation):
class Arguments:
book_data = BookInput(required=True)
book = graphene.Field(BookType)
@staticmethod
def mutate(root, info, book_data=None):
book_instance = Book.objects.get(pk=book_data.id)
if book_instance:
book_instance.title = book_data.title
book_instance.author = book_data.author
book_instance.year_published = book_data.year_published
book_instance.review = book_data.review
book_instance.save()
return UpdateBook(book=book_instance)
return UpdateBook(book=None)
The UpdateBook
mutation class is very similar to CreateBook
. The difference here is the logic in the mutate()
method, which retrieves a particular book object from the database by the book ID
provided and then applies the changes from the input argument to it.
Finally, let’s add a delete mutation. Add the code that follows at the bottom of api/schema.py
:
class DeleteBook(graphene.Mutation):
class Arguments:
id = graphene.ID()
book = graphene.Field(BookType)
@staticmethod
def mutate(root, info, id):
book_instance = Book.objects.get(pk=id)
book_instance.delete()
return None
In the DeleteBook
mutation class we have graphene.ID
as the only argument. The mutate()
method uses this id to remove the referenced book from the database.
We now have two queries and three mutations defined. To register these with Graphene, add the code below at the end of api/schema.py
:
class Mutation(graphene.ObjectType):
create_book = CreateBook.Field()
update_book = UpdateBook.Field()
delete_book = DeleteBook.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
Testing the GraphQL API
We are not ready to test the API. Let’s start the Django server:
$ python manage.py runserver
Now visit http://127.0.0.1:8000/graphql
in your browser. You should see the GraphIQL interface for interactive testing of the GraphQL API.
The black arrow in the diagram above is where you input your GraphQL code. Then you click on the play button in the top left corner of the screen to run the code and get a result in the area indicated with the blue arrow.
Issuing a query
Queries are used to request data from the server. The GraphQL code below is requesting all the books from the database. Enter it in the left-side panel of the GraphIQL interface.
query {
allBooks {
id
title
author
yearPublished
review
}
}
Press the play button to execute the query and see the results in the right-side panel.
Next try the following query, which requests a single book by its id
:
query {
book(bookId: 2) {
id
title
author
}
}
Note how each query can specify which of the attributes of the book model need to be returned.
Creating a book
The following GraphQL snippet defines a mutation that adds a new book to the database:
mutation createMutation {
createBook(bookData: {title: "Things Apart", author: "Chinua Achebe", yearPublished: "1985", review: 3}) {
book {
title,
author,
yearPublished,
review
}
}
}
Updating an existing book
The next GraphQL mutation updates the book with id=6
:
mutation updateMutation {
updateBook(bookData: {id: 6, title: "Things Fall Apart", author: "Chinua Achebe", yearPublished: "1958", review: 5}) {
book {
title,
author,
yearPublished,
review
}
}
}
Deleting a book
The final mutation example deletes the book with id=6
from the database:
mutation deleteMutation{
deleteBook(id: 6) {
book {
id
}
}
}
Testing the Book API with other GraphQL clients
Django CSRF prevents unauthenticated users on the website from performing malicious attacks. Given this, any POST request made from an external application outside the Django site will result in a 403 Forbidden Error
.
To avoid this there are two options. The most secure option is to add the CSRF token generated by the Django application to the POST
requests that your client makes to the GraphQL endpoint. See the Ajax section in the Django documentation to learn about this option.
An easier, but less secure option is to exempt the GraphQL endpoint from CSRF protection. To do that, open theapi/urls.py
file and change the definition of the GraphQL endpoint as follows:
from django.urls import path
from graphene_django.views import GraphQLView
from django.views.decorators.csrf import csrf_exempt
urlpatterns = [
...
path('graphql', csrf_exempt(GraphQLView.as_view(graphiql=True))),
]
The csrf_exempt
wrapper added to the GraphQLView
removes the CSRF token verification from the endpoint.
If you want to make sure the CSRF protection does not interfere with your GraphQL endpoint, you can use Postman to send GraphQL requests to the Django API:
Using the above screenshot as a reference, follow these steps to send a GraphQL request with Postman:
- Paste your GraphQL endpoint
http://127.0.0.1:8000/graphql
in the box with the purple arrow. - Click on the first white arrow pointing to the “Body” option
- Click on the GraphQL options at the second white arrow
- Paste your query code in the query box and click on the “Send” blue button.
- You will see the result of your API request at the bottom, in the light green arrow area.
- Notice the blue arrow area, where you should be making a GET request for queries, or a POST request for mutations.
Try the code snippets we used above to test our API through Postman.
Conclusion
In this tutorial we have created a simple GraphQL API in Django using the Graphene-Django package built on top of Graphene, which made it easy to add GraphQL functionality to our Django application.
We created queries to read data, mutations to write and change data and tested our API using the GraphIQL interface provided by the Graphene-Django library and the popular API client Postman. You can find the complete code for the project here.
Adeyemi Atoyegbe is a self-taught Python developer. You can find him on Twitter, GitHub.