Hana Belay
Posted on January 31, 2023
If you are a Django developer looking for something beyond the traditional REST APIs to add some extra power and flexibility to your projects, integrating GraphQL might be a good place to start.
GraphQL is a modern query language that gives you more control over your data and how it's retrieved. By combining the power of Django and GraphQL, you can unlock new capabilities and provide a more flexible and efficient way for clients to access and manipulate your data.
In this blog post, we will explore the benefits of using GraphQL with Django and how to integrate the two technologies.
Note:- If you encounter any issues throughout the tutorial, you can check out the code in the GitHub repository.
Table of Contents
- Prerequisite
- GraphQL Introduction
- Project Configuration
- Graphene-Django
- GraphQL Schema
- Load Data to the Database for Testing
- Testing the Schema
- GraphQL Mutation
- Conclusion
Prerequisite
This guide assumes that you have a basic understanding of the Django framework.
Introduction
GraphQL is a query language for an API that allows you to specify the data you want to retrieve or manipulate in a request. It is often used as an alternative (not a replacement) to REST APIs because:
- It allows the client to request and get only what it needs; nothing more, nothing less.
- It reduces the under-fetching or over-fetching of data.
- It allows you to batch multiple operations into a single request.
The terms we are going to often use throughout the tutorial are schema and mutation.
A GraphQL schema is the core of a GraphQL server that defines the structure of the data and the operations that can be performed on that data.
Mutations are GraphQL operations that allow you to modify data on the server. They are often used to create, update, or delete data in a database.
Project Configuration
First, create a virtual environment and activate it:
python3 -m venv .venv
source .venv/bin/activate
Next, install Django and create a new Django project:
pip install django==4.1.5
django-admin startproject config .
Run migrations:
python manage.py migrate
Create a superuser:
python manage.py createsuperuser
Run the server to ensure the installation worked correctly:
python manage.py runserver
Great, now let’s create a new app called products
. This app will be used to demonstrate the integration of Django and GraphQL.
python manage.py startapp products
Add it to the list of installed apps inside the settings:
# settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Local apps
"products"
]
For the purpose of this tutorial, let’s build a simple product model. A product can have tags and prices that are associated with it. Go to the models.py
file of the products
app and add the following code:
# products/models.py
from django.conf import settings
from django.db import models
from django.template.defaultfilters import slugify
from django.utils.translation import gettext_lazy as _
User = settings.AUTH_USER_MODEL
def get_image_filename(instance, filename):
name = instance.name
slug = slugify(name)
return f"products/{slug}-{filename}"
class ProductTag(models.Model):
name = models.CharField(
max_length=100, help_text=_("Designates the name of the tag.")
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self) -> str:
return self.name
class Product(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=200)
tags = models.ManyToManyField(ProductTag, blank=True)
desc = models.TextField(_("Description"), blank=True)
thumbnail = models.ImageField(upload_to=get_image_filename, blank=True)
url = models.URLField()
quantity = models.IntegerField(default=1)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ("-created_at",)
def __str__(self):
return self.name
class Price(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
price = models.DecimalField(decimal_places=2, max_digits=10)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self) -> str:
return f"{self.product.name} {self.price}"
- The
Product
model contains an image field for the thumbnail so let’s go ahead and install Pillow.
pip install pillow==9.3.0
Then, let’s register the models as follows:
# products/admin.py
from django.contrib import admin
from .models import Price, Product, ProductTag
class PriceAdmin(admin.StackedInline):
model = Price
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
inlines = (PriceAdmin,)
class Meta:
model = Product
admin.site.register(ProductTag)
admin.site.register(Price)
- The
Price
model is registered inline with theProduct
model so that we can add the prices of a product while creating the product inside the admin panel.
Since we are working with user-uploaded images, we need to set MEDIA_URL
and MEDIA_ROOT
in the settings:
# config/settings.py
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
Next, configure the project's urls.py
to serve user-uploaded media files during development.
# config/urls.py
from django.conf import settings
from django.conf.urls.static import static
# Media Assets
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Let’s now test the functionality we have so far:
py mangage.py makemigrations
py manage.py migrate
py mangage.py runserver
Go to the admin panel http://localhost:8000/admin/ to play around.
Graphene-Django
Graphene-Django is a library built on top of Graphene (a Python library for building GraphQL APIs).
It provides some additional abstractions that make it easy to add GraphQL functionality to your Django project.
Let’s install it:
pip install graphene-django==3.0.0
Add it to the list of installed apps in the settings:
# config/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"products",
"graphene_django", # here
]
Hurray! Let’s move on to the next section where the fun begins.
GraphQL Schema
A GraphQL schema is the core of a GraphQL server that defines the structure of the data and the operations that can be performed on that data. It specifies the types of data that can be queried or modified, as well as the relationships between those types.
In a GraphQL API, the schema serves as a contract between the server and the client. The client can send a query or mutation to the server that specifies the data it wants to retrieve or manipulate, and the server will only execute the request if it is valid according to the schema. This helps to ensure that the client can only access or modify data that is allowed by the schema.
Head over to the products app and create a file called schema.py
# products/schema.py
from graphene_django import DjangoObjectType
from .models import Price, Product, ProductTag
class ProductTagType(DjangoObjectType):
class Meta:
model = ProductTag
fields = ("id", "title")
class ProductType(DjangoObjectType):
class Meta:
model = Product
fields = "__all__"
class PriceType(DjangoObjectType):
class Meta:
model = Price
fields = "__all__"
- We have created GraphQL types for each of our Django models. The types inherit from
DjangoObjectType
-
DjangoObjectType
automatically defines GraphQL fields that correspond to fields on the Django models. If you are familiar with Django Rest Framework, it is similar toModelSerializer
Now we need a root type that specifies a set of operations and an entry point for queries. This is specified in the following Query
class:
# products/schema.py
import graphene
class Query(graphene.ObjectType):
tags = graphene.List(ProductTagType)
products = graphene.List(ProductType)
prices = graphene.List(PriceType)
def resolve_tags(self, info, *args, **kwargs):
return ProductTag.objects.all()
def resolve_products(self, info, **kwargs):
return Product.objects.all()
def resolve_prices(self, info, *args, **kwargs):
return Price.objects.all()
- The
Query
class has the types as fields (tags, products, prices) and resolver functions to query the database for a specific model.
Now that we have our root type Query
we need to pass it into the Schema
Add the following at the end of the schema.py
file:
schema = graphene.Schema(query=Query)
Below is the entire schema.py
file:
# products/schema.py
import graphene
from graphene_django import DjangoObjectType
from .models import Price, Product, ProductTag
class ProductTagType(DjangoObjectType):
class Meta:
model = ProductTag
fields = "__all__"
class ProductType(DjangoObjectType):
class Meta:
model = Product
fields = "__all__"
class PriceType(DjangoObjectType):
class Meta:
model = Price
fields = "__all__"
class Query(graphene.ObjectType):
tags = graphene.List(ProductTagType)
products = graphene.List(ProductType)
prices = graphene.List(PriceType)
def resolve_tags(root, info):
return ProductTag.objects.all()
def resolve_products(root, info):
return Product.objects.all()
def resolve_prices(root, info):
return Price.objects.all()
schema = graphene.Schema(query=Query, mutation=Mutation)
One of the biggest advantages of GraphQL is the ability to expose only one endpoint to the client. This is contrary to a REST API where we have to specify multiple endpoints.
Create a file named urls.py
inside the products app and add the following:
# products/urls.py
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView
from .schema import schema
app_name = "products"
urlpatterns = [
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))),
]
- Django by default has a CSRF middleware that provides protection against Cross-Site Request Forgeries.
- This middleware prevents frontend applications from posting to the
graphql
endpoint. One way to mitigate this is by wrapping theGraphQLView
with thecsrf_exempt
decorator.
Finally, update the project’s urls.py
file to include the URL configuration for the products
app:
# config/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("products.urls", namespace="products")),
]
Run the server and go to http://localhost:8000/graphql
Type in the following query and execute it:
{
products {
id
name
desc
url
quantity
}
}
The output will look something like this:
That’s because we don’t have any data. To create some data for testing, we are going to use fixtures in the next section.
Load Data to the Database for Testing
To populate our database with some data, let’s use fixtures. A fixture is a collection of data that Django knows how to import into a database.
At the root of your project, create a file named data.json
and add the following:
[
{
"model": "auth.user",
"pk": 2,
"fields": {
"password": "pbkdf2_sha256$390000$uVxVY1kKAqEwAbvQn0H3AX$A5EsaIcpX3eKVL71t9n1t/+wzZLvWIYtSqwERzW0gGU=",
"last_login": null,
"is_superuser": true,
"username": "test",
"first_name": "",
"last_name": "",
"email": "",
"is_staff": true,
"is_active": true,
"date_joined": "2023-01-09T06:44:06.618Z",
"groups": [],
"user_permissions": []
}
},
{
"model": "products.producttag",
"pk": 2,
"fields": {
"name": "Sci-Fi",
"created_at": "2023-01-09T06:07:41.890Z",
"updated_at": "2023-01-09T06:07:41.890Z"
}
},
{
"model": "products.producttag",
"pk": 3,
"fields": {
"name": "Horror",
"created_at": "2023-01-09T06:26:02.766Z",
"updated_at": "2023-01-09T06:26:02.766Z"
}
},
{
"model": "products.producttag",
"pk": 4,
"fields": {
"name": "Fantasy",
"created_at": "2023-01-09T06:26:21.166Z",
"updated_at": "2023-01-09T06:26:21.166Z"
}
},
{
"model": "products.product",
"pk": 1,
"fields": {
"user": [
"test"
],
"name": "2001: A Space Odyssey",
"desc": "This allegory about humanity’s exploration of the universe—and the universe’s reaction to humanity—is a hallmark achievement in storytelling that follows the crew of the spacecraft Discovery as they embark on a mission to Saturn. Their vessel is controlled by HAL 9000, an artificially intelligent supercomputer capable of the highest level of cognitive functioning that rivals—and perhaps threatens—the human mind.",
"thumbnail": "",
"url": "https://amzn.to/3leeblk",
"quantity": 7,
"created_at": "2023-01-09T06:28:49.806Z",
"updated_at": "2023-01-09T06:28:49.807Z",
"tags": [
2
]
}
},
{
"model": "products.product",
"pk": 2,
"fields": {
"user": [
"test"
],
"name": "It by Stephen King (1986)",
"desc": "It should come as no surprise that Stephen King shows up on this list of the top horror books more than once. He is, after all, the reigning king of terror. This novel taps into a pretty pervasive phobia: clowns. Pennywise, the killer clown, dwells in the sewers of Derry, Maine, and he preys upon the young residents of the town by shapeshifting into their deepest fears. The group of kids (the so-called Losers Club) must band together to defeat him.",
"thumbnail": "",
"url": "https://www.amazon.com/dp/1444707868?tag=readerwp-20",
"quantity": 1,
"created_at": "2023-01-09T06:30:53.032Z",
"updated_at": "2023-01-09T06:30:53.032Z",
"tags": [
3
]
}
},
{
"model": "products.product",
"pk": 3,
"fields": {
"user": [
"test"
],
"name": "The Eye of the World, by Robert Jordan",
"desc": "The Dark One, an evil force imprisoned inside a weakening cell, threatens the world. The Dragon Reborn, a reincarnated savior with powerful magical abilities, is born to a dying warrior woman on the slopes of a snowy mountain. Accompanied by a band of village youths, he seeks to defeat the Dark One, in The Wheel of Time, the sprawling, 14-book series. The depth of world-building is incredible, the characters indelible, and it comes to satisfying conclusion.",
"thumbnail": "",
"url": "https://www.amazon.com/dp/1250832365?ots=1&slotNum=1&imprToken=38c2ca9c-fad5-d50a-6ce&linkCode=ogi&tag=oprah-auto-20&ascsubtag=%5Bartid%7C10072.g.41315629%5Bsrc%7C%5Bch%7C%5Blt%7Csale%5Bpid%7C09d26eb0",
"quantity": 1,
"created_at": "2023-01-09T06:31:56.008Z",
"updated_at": "2023-01-09T06:31:56.008Z",
"tags": [
4
]
}
},
{
"model": "products.price",
"pk": 1,
"fields": {
"product": 1,
"price": "100.00",
"created_at": "2023-01-09T06:28:49.808Z",
"updated_at": "2023-01-09T06:28:49.808Z"
}
},
{
"model": "products.price",
"pk": 2,
"fields": {
"product": 2,
"price": "90.00",
"created_at": "2023-01-09T06:30:53.034Z",
"updated_at": "2023-01-09T06:30:53.034Z"
}
},
{
"model": "products.price",
"pk": 3,
"fields": {
"product": 3,
"price": "70.00",
"created_at": "2023-01-09T06:31:56.009Z",
"updated_at": "2023-01-09T06:31:56.009Z"
}
}
]
Then, run the following command:
python manage.py loaddata data.json
You can log in to the admin panel with the following credentials and see the test data.
username: test
password: 123django
Tip:- To dump data into JSON you can use the following command:
python manage.py dumpdata --natural-foreign --exclude=auth.permission --exclude=contenttypes --exclude=sessions --exclude=admin.logentry --indent=4 > data.json
Testing the Schema
If you now go back to http://localhost:8000/graphql and type in the previous query, you will see the following output:
Interesting. To get related model fields like product tags and prices, you can use the following query:
{
products {
id
name
desc
url
quantity
priceSet {
id
}
tags {
id
}
}
}
Follow up: write a query to fetch products of the same tag.
For image fields, consider using the library Graphene-file-upload because file uploading is not natively implemented in Graphene.
GraphQL Mutation
In GraphQL, a mutation is an operation that allows you to modify data through create, update, or delete operations.
Graphene-Django makes it really easy to perform mutations.
To demonstrate this, let’s create a mutation for creating a product tag. Inside the schema.py
file, add the following class:
# products/schema.py
class CreateProductTag(graphene.Mutation):
class Arguments:
name = graphene.String(required=True)
product_tag = graphene.Field(ProductTagType)
def mutate(self, info, name):
product_tag = ProductTag(name=name)
product_tag.save()
return CreateProductTag(product_tag=product_tag)
- The inner class is used to specify the arguments for the create operation.
- The class attribute
product_tag
specifies the response of the mutation.
Just like we created a Query
class as an entry point for queries, we also need to create a Mutation
class to use as an entry point for mutations:
# products/schema.py
class Mutation(graphene.ObjectType):
create_product_tag = CreateProductTag.Field()
Update the schema to include mutation:
# products/schema.py
schema = graphene.Schema(query=Query, mutation=Mutation)
Run the server and type the following mutation to test it:
mutation {
create_product_tag: createProductTag(name: "Comedy") {
productTag {
id
name
}
}
}
Press the play button and you will see the following output:
Note:- Graphene automatically camelcases all field names for better compatibility with JavaScript clients.
Let’s do the same thing for update and delete operations:
# products/schema.py
class UpdateProductTag(graphene.Mutation):
class Arguments:
id = graphene.Int()
name = graphene.String(required=True)
product_tag = graphene.Field(ProductTagType)
def mutate(root, info, id, name):
tag = ProductTag.objects.get(id=id)
tag.name = name
return UpdateProductTag(product_tag=tag)
class DeleteProductTag(graphene.Mutation):
class Arguments:
id = graphene.Int()
ok = graphene.Boolean()
def mutate(root, info, id):
ProductTag.objects.get(id=id).delete()
return DeleteProductTag(ok=True)
class Mutation(graphene.ObjectType):
create_product_tag = CreateProductTag.Field()
update_product_tag = UpdateProductTag.Field()
delete_product_tag = DeleteProductTag.Field()
Below is the updated schema.py
file:
# products/schema.py
import graphene
from graphene_django import DjangoObjectType
from .models import Price, Product, ProductTag
class ProductTagType(DjangoObjectType):
class Meta:
model = ProductTag
fields = "__all__"
class ProductType(DjangoObjectType):
class Meta:
model = Product
fields = "__all__"
class PriceType(DjangoObjectType):
class Meta:
model = Price
fields = "__all__"
class Query(graphene.ObjectType):
tags = graphene.List(ProductTagType)
products = graphene.List(ProductType)
prices = graphene.List(PriceType)
def resolve_tags(root, info):
return ProductTag.objects.all()
def resolve_products(root, info):
return Product.objects.all()
def resolve_prices(root, info):
return Price.objects.all()
class CreateProductTag(graphene.Mutation):
class Arguments:
name = graphene.String(required=True)
product_tag = graphene.Field(ProductTagType)
def mutate(root, info, name):
product_tag = ProductTag(name=name)
product_tag.save()
return CreateProductTag(product_tag=product_tag)
class UpdateProductTag(graphene.Mutation):
class Arguments:
id = graphene.Int()
name = graphene.String(required=True)
product_tag = graphene.Field(ProductTagType)
def mutate(root, info, id, name):
tag = ProductTag.objects.get(id=id)
tag.name = name
return UpdateProductTag(product_tag=tag)
class DeleteProductTag(graphene.Mutation):
class Arguments:
id = graphene.Int()
ok = graphene.Boolean()
def mutate(root, info, id):
ProductTag.objects.get(id=id).delete()
return DeleteProductTag(ok=True)
class Mutation(graphene.ObjectType):
create_product_tag = CreateProductTag.Field()
update_product_tag = UpdateProductTag.Field()
delete_product_tag = DeleteProductTag.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
Alright, let’s test the operations.
Delete a product tag
Type in the following query to delete a product tag:
mutation{
deleteProductTag(id:5){
ok
}
}
You will get an output like this:
{
"data": {
"deleteProductTag": {
"ok": true
}
}
}
Update a product tag
And for an update try the following query:
mutation {
updateProductTag(id: 2, name: "Updated Sci-Fi") {
productTag{
id
name
}
}
}
The response is going to be:
{
"data": {
"updateProductTag": {
"productTag": {
"id": "2",
"name": "Updated Sci-Fi"
}
}
}
}
So far we have seen the CRUD operations on ProductTag
model. As a follow-up, you can experiment with the other models.
Tip: One important feature if you use Django Rest Framework is that you can reuse your serializers with Graphene Django mutations! You can create a Mutation based on a serializer by using the SerializerMutation
base class:
from graphene_django.rest_framework.mutation import SerializerMutation
from .serializers import ProductSerializer
class CreateProduct(SerializerMutation):
class Meta:
serializer_class = ProductSerializer
Conclusion
In conclusion, the Graphene-Django library makes it easy to get started with GraphQL integration by allowing you to use your existing Django models as the data source for your GraphQL API and automatically generate the schema. For more information, check out the documentation.
Furthermore, by leveraging the built-in authentication and permission systems of Django, you can easily control access to your GraphQL API. With all of these benefits, GraphQL integration is definitely worth considering for your next Django project.
Note:- Some of the sections of this tutorial (the first two paragraphs and the conclusion) were written with the help of an AI.
If you got lost somewhere throughout the guide, check out the project on GitHub
Happy coding! 🖤
Posted on January 31, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.