Blog /

Elevate Your Django Projects with GraphQL Integration

python

Elevate Your Django Projects with GraphQL Integration

March 23, 202314 minutes
Elevate Your Django Projects with GraphQL Integration

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

  1. Prerequisite
  2. GraphQL Introduction
  3. Project Configuration
  4. Graphene-Django
  5. GraphQL Schema
  6. Load Data to the Database for Testing
  7. Testing the Schema
  8. GraphQL Mutation
  9. 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 the Product 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 to ModelSerializer

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 the GraphQLView with the csrf_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:

Graphql output

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:

Graphql 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
    }
  }
}
Graphql output

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:

Graphql 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! 🖤

Explore other topics