Unveiling GraphQL in Django: A straightforward guide to modern API development.
GraphQL, a not-so-new tool on the block. Almost every new and old software developer has heard of it, if not played with it. As a developer who recently delved into it, I’m here to share some knowledge with you.
The Introduction
Facebook is credited with creating GraphQL, which consists of a runtime layer and a query language. It is a query language used on the frontend to request specific data from web APIs.
On the side of a runtime layer, it processes and validates queries against the defined data schema, that are being sent to the server, and the requested data is returned.
As a query language, it is made up of three operations:
- Query
- Mutation
- Subscription
The query and mutation operations are used to perform actions that are analogous to HTTP verbs (POST, READ, PATCH, DELETE). The query operation works just like the HTTP-READ action, requesting data from the server, while the mutation operation is responsible for data creation and modification (HTTP-POST, PATCH, DELETE).
For the subscription operation, think of it as the WebSocket connection for GraphQL APIs. While the query operation receives an API response after a single request, the subscription reads data continuously in real-time.
When using GraphQL, data queries can be sent using HTTP-POST or HTTP-GET actions. By now, you should realize that queries are the specific data we need from a GraphQL API.
Now, what do queries actually look like?
Let’s say we want to be greeted by a GraphQL API as soon as we wake up in the early hours of the morning. We can send a data query to the API, requesting a "Hello, {first_name}, what a wonderful day to try again” message.
{
greeting{
hello
}
}
We would get a response like this:
{
"data": {
"greeting": {
"hello": "Hello Prince, what a wonderful day to try again"
}
}
}
We could as well send another data query to the API, requesting a “Goodbye, and take care” message.
{
greeting{
goodbye
}
}
We would get a response like this:
{
"data": {
"greeting": {
"goodbye": "Goodbye, and take care"
}
}
}
While it is possible to send queries that look like this:
{
hello
}
or this:
{
goodbye
}
The previous examples were given to show that we can specify the data we need from the greeting field.
This type of request has an unnamed data query, where a certain type of operation for the request is not given. By default, this is a request using the query operation with HTTP-GET.
The greeting field is a resolver that leads to the hello and goodbye resolvers to fetch the data (messages) we need. This means that we can request both hello and goodbye messages at the same time.
{
greeting{
hello
goodbye
}
}
{
"data": {
"greeting": {
"hello": "Hello Prince, what a wonderful day to try again"
"goodbye": "Goodbye, and take care"
}
}
}
Another type of data query is one with a named operation. Here, the client gives the type of operation, along with a name, that should be performed with the HTTP-POST request. Multiple related requests can be sent at once to the API.
query InteractWithUser{
greeting{
hello
}
assitance{
checkTime
}
}
{
"data": {
"greeting": {
"hello": "Hello Prince, what a wonderful day to try again"
},
"assistance": {
"checkTime": "The current time is 07:34 AM"
}
}
}
Now, you must have noticed the word, resolver, before reaching this point. Each field in a data query is tied to a function called a resolver. This function, implemented on the runtime layer, is responsible for defining how data is processed and presented in the API response whenever its field is called. This is where the magic is created.
On the part of the runtime layer, this is the server-side of a GraphQL application. It reads the data query requested from the query language, fetches the data from the data sources, and gives a response with the exact data that was requested, according to the schema that was defined.
A schema defines the structure of data that can be queried. Each schema has fields of different data types, which are referred to as schema types. These fields are what will be used in the query language to request the exact data needed.
A GraphQL server uses a Schema Definition Language (SDL) to define the schema of data... just as the name implies. The SDL is made up of several schema types; some of them include:
- Scalar type
- Object type
- Query and Mutation types
- Enumeration type
- Input type
SCALAR TYPE: This schema type is the most basic form of data. Examples include: String
, Int
, Float
, Boolean
.
OBJECT TYPE: This is the schema type that represents an entity from a data source, along with the fields it has. This is the type needed by the client application. An example of such a type is:
type Student{
pk: Int
name: String
department: String
}
QUERY AND MUTATION TYPES: These are the main schemas that the query language uses to communicate with data sources in order to indicate which fields should be queried or manipulated when retrieving or storing data in a database. The Query type fetches the data needed by the client, from a data source. The Mutation type creates and modifies already-existing data in a data source. Both types operate with fields.
type Query{
students: [Students]
}
type Mutation{
register(name: String, department: String): Student
}
From the Query
schema, a list of students is returned when the students
field is called. And with the Mutation
schema, a student record is created and returned with the name
and department
arguments given in the register
field.
ENUMERATION TYPE, or, as sometimes I like to call it, “the schema of choice," is used to select an option from a predefined set of choices. This is effective when you want to limit the possible values for a field when working with the mutation operation.
enum ClassLevel {
JUNIOR
SENIOR
}
type Student{
pk: String
name: String
department: String
classLevel: ClassLevel
}
type Mutation{
register(name: String, department: String, classLevel: ClassLevel): Student
}
INPUT TYPE: From the last example of Enumeration type, we have a Mutation schema defined with a register
field, having three arguments; name
, department
, and classLevel
. While it is okay to pass data to a mutation with one argument after another, we can define a schema that passes this data to the operation.
enum ClassLevel {
JUNIOR
SENIOR
}
type Student{
pk: String
name: String
department: String
classLevel: ClassLevel
}
input StudentInput{
name: String
department: String
classLevel: ClassLevel
}
type Mutation{
register(input: StudentInput): Student
}
The Analogy of GraphQL and REST
While both API architectural styles are widely popular, GraphQL shows advantages over REST.
Solving overfetching with GraphQL. GraphQL allows the client to precisely request the data needed; every other irrelevant data is omitted from the response. It minimizes unnecessary data transfers. This is not the same case with REST, as it will return all the properties of a data resource when requested.
If we had a REST endpoint that retrieved a student’s information,
- GET
students/{name}
We would get a response showing all the details of the resource without actually selecting the specific data we wanted.
{
"data": {
"name": "Isaac",
"department": "Physics",
"classLevel": "Junior"
}
}
Whereas with GraphQL, we could specify that we just want the name
and department
information of the student.
query {
student(name: "Isaac"){
name
department
}
}
{
"data": {
"student": {
"name": "Isaac",
"department": "Physics"
}
}
}
Solving underfetching with GraphQL. Imagine a scene where our client application wants to fetch multiple pieces of information from our database, and display them on a page in our application. Let’s say:
- Fetching details about a student
- Retrieving their course enrollment history
- Obtaining information about all departments
We have multiple REST endpoints that do those:
- GET
students/{name}
- GET
courses-enrolled?student=Isaac
- GET
departments
This would mean making multiple API requests with these endpoints, and combining their responses to fit our UI design.
But with GraphQL, we can send a single request to a single /graphql
endpoint with a named Query operation to our API, and all the data we need will be returned.
query PageUI{
student(name: "Isaac"){
name
department
classLevel
}
coursesEnrolled(student: "Isaac"){
name
description
}
departments{
name
headOfDepartment
}
}
{
"data": {
"student": {
"name": "Isaac",
"department": "Physics",
"classLevel": "Junior"
},
"coursesEnrolled": [
{
"name": "Quantum Mechanics",
"description": "Exploring the principles of quantum physics."
},
{
"name": "Mathematical Methods in Physics",
"description": "Application of mathematical techniques in solving physical problems."
}
],
"departments": [
{
"name": "Physics",
"headOfDepartment": "Prof. Anderson"
},
{
"name": "Chemistry",
"headOfDepartment": "Dr. Brown"
}
]
}
}
GraphQL solves the problem of making multiple API calls to get a sufficient amount of data for an application.
Django
“The web framework for perfectionists with deadlines"... well, I’m not so sure about that statement. But we know it’s used for building full-stack web applications and RESTful APIs with the Django REST Framework. There was a time when I never thought it could build GraphQL APIs as well, until I came across Graphene, a framework for building GraphQL APIs with Django. I played with it a bit on a dummy project. I thought I was enjoying it, then I came across Ariadne, another Python framework for GraphQL with Django.
Comparing my experience with Graphene to Ariadne, I felt restricted using Graphene. It was like I had to follow a lot of rules. There were so many Python classes written for just a dummy project. Plus, it took me away from GraphQL schema types. I wanted to write my own schemas, just like I saw them in the GraphQL documentation. Graphene isn’t bad at all, but Ariadne feels better. All I have to do is define my data models, write my schemas and resolver functions, connect them to the Mutation and Query types, and make the schemas executable.
Another framework available for writing GraphQL APIs is Strawberry; I've not tried it and will not be doing so anytime soon. This article is not to convince you to pick any framework, but if you’d be following with Ariadne, this one is for you.
Demo…
For the sake of simplicity, the topic of authentication is outside the scope of this article. We will be creating a simple GraphQL API called Blog-QL.
Create the project directory from your terminal by running the command:
$ mkdir blog-QL
Open the directory with your favourite code editor. After this, create a Python virtual environment in the code directory and activate it.
virtualenv env
source env/bin/activate
Install Ariadne for Django with the command:
(env) pip install ariadne_django
This installs Django and Ariadne packages at once, together with other packages needed for our API.
Up next, create a Django project called core in the current directory:
django-admin startproject core .
Add the package, ariadne_django to the list of installed apps in the settings.py file:
INSTALLED_APPS = [
...
"ariadne_django",
]
Now we’ve installed the Ariadne package and registered it in our Django project. We now have to create a Django application, "blog," with which we will be writing. We do this with the command:
python manage.py startapp blog
Add the blog app to the list of installed apps:
INSTALLED_APPS = [
# 3rd party apps
"ariadne_django",
# local apps
"blog.apps.BlogConfig",
]
Unlike the normal routine for building applications with Django (fullstack and REST API), where we have to create a urls.py
file for each Django app created, and register it in the urlpatterns
of the project’s mainurls.py
file, when building a GraphQL API, “we don't do that here”.
This is because every piece of data that we will ever create and request will be done from the/graphql/
endpoint.
To get through the next stage without an error, create a schema.py
file in the core’s directory. Create a dummy schema in the file and make it executable.
from ariadne import QueryType, make_executable_schema
type_defs = """
type Query {
hello: String!
}
"""
query = QueryType()
## the decorator attaches the "hello" field from the Query schema
## to the resolver function
@query.field("hello")
def resolve_hello(*_):
return "Hello world!"
schema = make_executable_schema(type_defs, query)
From the code above, we defined a Query schema type, with which we would resolve the field, “hello”, to return a string value. We also defined a resolver function resolve_hello
to returnHello world!
whenever the “hello” field is queried. In the end, we make our schema executable with the make_executable_schema
function.
Edit our core’s urls.py
file to look like this:
from django.contrib import admin
from django.urls import path
from ariadne_django.views import GraphQLView
from .schema import schema
urlpatterns = [
path('admin/', admin.site.urls),
path('graphql/', GraphQLView.as_view(schema=schema), name='graphql'),
]
We now have two endpoints, one for our admin site and the other for all GraphQL API requests. At this point, we can now test our API with the hello
to see that it has been properly set.
Apply migrations for Django system apps, and start the server:
python3 manage.py migrate
python3 manage.py runserver
Send a request to the GraphQL endpoint, http://127.0.0.1:8000/graphql/ with a hello
field in the query, from your API client. You should see a “Hello world!” message in the response.
Now let’s flesh out our blog API. Recall that I mentioned that authentication is out of the scope of the article. So there is no password creation or validation for our author’s account.
In the models.py
of our blog
app, create an Author
model:
from django.db import models
class Author(models.Model):
username = models.CharField(max_length=20, unique=True)
age = models.PositiveIntegerField()
def __str__(self) -> str:
return self.name
A simple name
and age
field will do for our model.
Create migrations for the model and migrate:
python3 manage.py makemigrations blog
python3 manage.py migrate
In our previous code, we defined a Query schema directly in our schema.py
file. While there is nothing wrong with that, we can create our schema types in GraphQL files to be called in the schema.py
file. This is best for modularization, where we may have multiple schema types for multiple models, in multiple Django apps.
In the parent directory, create a folder, schemas
. This will contain all GraphQL files for schemas.
In the folder, create a author.blog.graphql
file. This will contain object schema types for our author and blog models:
type Author{
username: String
age: Int
}
input AuthorInput{
username: String!
age: Int!
}
type AuthorResponse{
message: String
author: Author
}
type Mutation{
createAuthor(input: AuthorInput): AuthorResponse
}
type Query{
hello: String
}
Here we defined an Author
object schema type for our author model. We also defined an input schema type with which we would be providing data, when creating an author with the mutation operation. We would get a response containing a message and our newly created author.
Register the schemas created in the schema.py
file in our core’s directory.
# core/schema.py
from ariadne import QueryType, make_executable_schema, load_schema_from_path, MutationType
type_defs = load_schema_from_path("schemas")
query = QueryType()
mutation = MutationType()
schema = make_executable_schema(type_defs, mutation, query)
In the code, we imported the load_schema_from_path
function that will read every schema type in every GraphQL file in the schemas directory. The QueryType and MutationType classes are responsible for our query and mutation operations, and schemas are made executable. Yeah, it’s that simple.
Let’s create our resolver function for creating an author.
In the blog’s app directory, create a resolvers.py
file, which will contain all our resolver functions for the blog app.
from .models import Author
def resolve_create_author(*_, input: dict):
author = Author.objects.create(username=input['username'], age=input['age'])
author.save()
return {
"message": "Author created",
"author": author
}
The resolver function takes two parameters: *_
and input
.
The *
prefix says that the function works with GraphQL-related parameters that don’t need to be listed as parameters. The underscore is simply a variable, which will not be used, but it represents the parent object of the function, which in this case is the mutation object.
The input
parameter will be responsible for passing our data into the query for the mutation operation.
Edit our schema.py
file to look like this:
from ariadne import QueryType, make_executable_schema, load_schema_from_path, MutationType
from blog import resolvers as blog_resolvers
type_defs = load_schema_from_path("schemas")
query = QueryType()
mutation = MutationType()
mutation.set_field('createAuthor', blog_resolvers.resolve_create_author)
schema = make_executable_schema(type_defs, mutation, query)
The mutation.set_field
function ties our createAuthor
field of the Mutation schema type to the resolver function responsible for creating an author.
Testing this on our API client, provide the data query to the GraphQL endpoint:
mutation{
createAuthor(input:{
username: "TheAlchemist",
age: 123
}){
message
author{
username
age
}
}
}
We should get the following response:
{
"data": {
"createAuthor": {
"message": "Author created",
"author": {
"username": "TheAlchemist",
"age": 123
}
}
}
}
We created an author object with the input argument, which represents the input schema type, and we specified the data we needed to be returned. After we have created an author, we should create a blog for the author.
Define a blog data model in the models.py
file in the blog app’s directory.
from django.db import models
class Author(models.Model):
username = models.CharField(max_length=20, unique=True, default="")
age = models.PositiveIntegerField()
def __str__(self) -> str:
return self.name
class Blog(models.Model):
title = models.CharField(max_length=20)
content = models.TextField()
author = models.ForeignKey(Author, on_delete=models.CASCADE)
def __str__(self) -> str:
return self.title
Make migrations for the new model, and migrate:
python3 manage.py makemigrations blog
python3 manage.py migrate
Make changes to the author.blog.graphql
file:
...
type Blog{
pk: Int
title: String
content: String
author: Author
}
type BlogInput{
authorUsername: String
title: String
content: String
}
type BlogResponse{
message: String
blog: Blog
}
type Mutation{
createAuthor(input: AuthorInput): AuthorResponse
postBlog(input: BlogInput): BlogResponse
}
Create the resolver function for posting the blog by the author:
def resolve_post_blog(*_, input:dict):
try:
author = Author.objects.get(username=input['authorUsername'])
blog = Blog.objects.create(title=input['title'], content=input['content'], author=author)
blog.save()
return{
"message": "Blog posted",
"blog": blog
}
except Author.DoesNotExist:
raise Exception("Author does not exist")
Attach the resolver function to the postBlog
field of our Mutation schema type in our schema.py
file:
from ariadne import QueryType, make_executable_schema, load_schema_from_path, MutationType
from blog import resolvers as blog_resolvers
type_defs = load_schema_from_path("schemas")
query = QueryType()
mutation = MutationType()
mutation.set_field('createAuthor', blog_resolvers.resolve_create_author)
mutation.set_field('postBlog', blog_resolvers.resolve_post_blog) # new
schema = make_executable_schema(type_defs, mutation, query)
Test the update:
mutation{
postBlog(input:{
authorUsername: "TheAlchemist",
title: "My first blog"
content: "This is my first blog, man"
}){
message
blog{
pk
title
content
author{
username
}
}
}
}
The response would be:
{
"data": {
"postBlog": {
"message": "Blog posted",
"blog": {
"pk": 1,
"title": "My first blog",
"content": "This is my first blog, man",
"author": {
"username": "TheAlchemist"
}
}
}
}
}
Having played with the mutation operation a little, let’s work on the query operation.
Make some changes to the author.blog.graphql
file:
type BlogsResponse{
message: String
blogs: [Blog]
}
type Query{
getBlogs: BlogsResponse
}
Create the resolver function for getting all blogs:
def resolve_get_blogs(*_):
blogs = Blog.objects.all()
return {
"message": "Blog-QL blogs",
"blogs": blogs
}
Attach the resolver function to the getBlogs
field of the Query schema type:
from ariadne import QueryType, make_executable_schema, load_schema_from_path, MutationType
from blog import resolvers as blog_resolvers
type_defs = load_schema_from_path("schemas")
query = QueryType()
mutation = MutationType()
mutation.set_field('createAuthor', blog_resolvers.resolve_create_author)
mutation.set_field('postBlog', blog_resolvers.resolve_post_blog)
query.set_field('getBlogs', blog_resolvers.resolve_get_blogs) # new
schema = make_executable_schema(type_defs, mutation, query)
Test the update:
query{
getBlogs{
message
blogs{
pk
title
content
}
}
}
We would get the following response:
{
"data": {
"getBlogs": {
"message": "Blog-QL blogs",
"blogs": [
{
"pk": 1,
"title": "My first blog",
"content": "This is my first blog, man"
},
{
"pk": 2,
"title": "My second blog",
"content": "This is my second blog, man"
}
]
}
}
}
What if we wanted to fetch a single blog? Just like the mutation operation, the query operation also works with arguments, but this time for data fetching.
type Query{
getBlogs: BlogsResponse
getBlog(pk: Int): BlogResponse
}
Create a resolver function for fetching a single blog article:
def resolve_get_blog(*_, pk):
try:
blog = Blog.objects.get(id=pk)
return{
"message": f"Article {blog.id}",
"blog": blog
}
except Blog.DoesNotExist:
raise Exception("Blog does not exist")
Attach the resolver function to the getBlog
field of the Query schema type:
from ariadne import QueryType, make_executable_schema, load_schema_from_path, MutationType
from blog import resolvers as blog_resolvers
type_defs = load_schema_from_path("schemas")
query = QueryType()
mutation = MutationType()
mutation.set_field('createAuthor', blog_resolvers.resolve_create_author)
mutation.set_field('postBlog', blog_resolvers.resolve_post_blog)
query.set_field('getBlogs', blog_resolvers.resolve_get_blogs)
query.set_field('getBlog', blog_resolvers.resolve_get_blog) # new
schema = make_executable_schema(type_defs, mutation, query)
Test the update:
query{
getBlog(pk:2){
message
blog{
title
content
author{
username
}
}
}
}
The response would be:
{
"data": {
"getBlog": {
"message": "Article 2",
"blog": {
"title": "My second blog",
"content": "This is my second blog, man",
"author": {
"username": "TheAlchemist"
}
}
}
}
}
Conclusion
In this guide, we’ve journeyed through the integration of GraphQL in Django, exploring the fundamental concepts, setting up the environment, and showcasing a hands-on demo. As we wrap up, let’s recap the key points and emphasize the significance of GraphQL in modern API development with Django.
Understanding the Shift to GraphQL: We’ve seen how GraphQL differs from traditional REST APIs, offering flexibility and efficiency in data retrieval. The ability to request only the required data empowers clients and optimizes performance.
Practical Implementation: Through our demonstration, you’ve witnessed the practical side of incorporating GraphQL into a Django project. From defining schemas to executing queries and mutations, you now have a foundation for building robust APIs with Django and GraphQL.
To wrap up, combining Django with GraphQL brings a fresh breeze to API development. So, go ahead, infuse your Django projects with the GraphQL mojo, and watch as your coding efforts spark new innovations.
Happy coding, and may your projects be as smooth as a well-written line of code.