Elevate Your Django Projects with GraphQL Integration
Tags: python, django, graphql, webdev
Published: 2023-01-31 07:38:01

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](https://github.com/earthcomfy/django_graphql)._

## Table of Contents

1. [Prerequisite](#prerequisite)
2. [GraphQL Introduction](#intro)
3. [Project Configuration](#config)
4. [Graphene-Django](#graphene)
5. [GraphQL Schema](#schema)
6. [Load Data to the Database for Testing](#fixture)
7. [Testing the Schema](#testing)
8. [GraphQL Mutation](#mutation)
9. [Conclusion](#conclusion)

## Prerequisite <a name="prerequisite"></a>

This guide assumes that you have a basic understanding of the Django framework.

## Introduction <a name="intro"></a>

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 <a name="config"></a>

First, create a virtual environment and activate it:

```bash
python3 -m venv .venv
source .venv/bin/activate
```

Next, install Django and create a new Django project:

```bash
pip install django==4.1.5
django-admin startproject config .
```

Run migrations:

```bash
python manage.py migrate
```

Create a superuser:

```bash
python manage.py createsuperuser
```

Run the server to ensure the installation worked correctly:

```bash
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.

```bash
python manage.py startapp products
```

Add it to the list of installed apps inside the settings:

```python
# 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:

```python
# 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](https://pypi.org/project/Pillow/).

```bash
pip install pillow==9.3.0
```

Then, let’s register the models as follows:

```python
# 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:

```python
# 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.**

```python
# 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:

```bash
py mangage.py makemigrations
py manage.py migrate
py mangage.py runserver
```

Go to the admin panel [http://localhost:8000/admin/](http://localhost:8000/admin/) to play around.

## Graphene-Django <a name="graphene"></a>

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:

```bash
pip install graphene-django==3.0.0
```

Add it to the list of installed apps in the settings:

```python
# 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 name="schema"></a>

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`

```python
# 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:

```python
# 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:

```python
schema = graphene.Schema(query=Query)
```

Below is the entire `schema.py` file:

```python
# 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:

```python
# 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:

```python
# 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](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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v9oxxdxadw0pl0phfp9d.png)

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 <a name="fixture"></a>

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:

```json
[
{
    "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:

```bash
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
python manage.py dumpdata --natural-foreign --exclude=auth.permission --exclude=contenttypes --exclude=sessions --exclude=admin.logentry --indent=4 > data.json
```

## Testing the Schema <a name="testing"></a>


If you now go back to [http://localhost:8000/graphql](http://localhost:8000/graphql) and type in the previous query, you will see the following output:


![Graphql output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s3lctv4r9w76kbpqk8dt.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/db9ggi8c63263wx0y64f.png)

**Follow up:** write a query to fetch products of the same tag.

For image fields, consider using the library [Graphene-file-upload](https://pypi.org/project/graphene-file-upload/) because file uploading is not natively implemented in Graphene.

## GraphQL Mutation <a name="mutation"></a>

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:

```python
# 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:

```python
# products/schema.py

class Mutation(graphene.ObjectType):
    create_product_tag = CreateProductTag.Field()
```

Update the schema to include mutation:

```python
# products/schema.py

schema = graphene.Schema(query=Query, mutation=Mutation)
```

Run the server and type the following mutation to test it:

```python
mutation {
  create_product_tag: createProductTag(name: "Comedy") {
    productTag {
      id
      name
    }
  }
}
```

Press the play button and you will see the following output:

![Graphql output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ng6fmbzhbadr15euobaq.png)

_**Note:-** Graphene automatically camelcases all field names for better compatibility with JavaScript clients._

Let’s do the same thing for update and delete operations:

```python
# 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:

```python
# 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:

```python
from graphene_django.rest_framework.mutation import SerializerMutation
from .serializers import ProductSerializer

class CreateProduct(SerializerMutation):
    class Meta:
        serializer_class = ProductSerializer
```

## Conclusion <a name="conclusion"></a>

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](https://docs.graphene-python.org/projects/django/en/latest/).

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](https://github.com/earthcomfy/django_graphql)

Happy coding! 🖤