Developing GraphQL APIs in Django with Strawberry

Last updated July 5th, 2024

In the ever-evolving landscape of web development, delivering efficient and flexible APIs is crucial. While RESTful APIs have been a staple for many years, GraphQL has emerged as a powerful alternative, offering more flexibility and efficiency in data retrieval. By allowing clients to request exactly what they need, GraphQL reduces over-fetching and under-fetching of data, enhancing the performance and usability of your applications.

Among the numerous GraphQL libraries available, Strawberry GraphQL stands out for its simplicity and ease of use. Built with a focus on type-safety and modern Python features, Strawberry leverages Python's type hints to create GraphQL schemas effortlessly. And, it's support for multiple integrations, allowing you to use it with your favorite web framework, makes it an excellent choice for Python developers looking to integrate GraphQL into their projects.

On the other hand, Django, a high-level Python web framework, has been a go-to choice for developers due to its robust feature set and "batteries-included" philosophy. Django's scalability, security features, and ease of use make it an ideal framework for building web applications of any size.

In this tutorial, we'll explore how to integrate Strawberry with Django to create a powerful and flexible API. We'll walk through setting up a Django project, defining models, and creating GraphQL queries and mutations with Strawberry. By the end of this tutorial, you'll have a functional GraphQL API powered by Django, ready to serve your data needs with precision and efficiency.

Let's get started!

Contents

Why Strawberry

Strawberry is a modern Python library designed to simplify the creation of GraphQL APIs.

Here are a few reasons why Strawberry is an excellent choice for building GraphQL APIs:

  1. Type Safety: Strawberry leverages Python's type hints to create GraphQL schemas. This helps ensure that your code is type-safe, which can reduce bugs and improve maintainability.
  2. Simplicity and Ease of Use: Strawberry is designed with simplicity in mind. Its API is intuitive and easy to learn, making it accessible for both beginners and experienced developers.
  3. Modern Python Features: Strawberry makes full use of Python's modern features, such as data classes and async/await syntax. This allows for cleaner, more readable code.
  4. Integration with Popular Python Frameworks: Strawberry offers seamless integration with popular Python frameworks like Django, Flask, etc., with examples to get you started. This makes it easy to add GraphQL capabilities to almost all existing web applications easily.
  5. Community and Support: Strawberry has an active and growing community. The developers are responsive, and there are plenty of resources available to help you get started and troubleshoot issues.

By choosing Strawberry, you're opting for a library that is both powerful and user-friendly, making it easier to build and maintain your GraphQL APIs.

Why Django

Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design.

Here are some reasons why Django is a preferred choice for web development:

  1. Batteries Included: Django comes with a lot of built-in features, such as an ORM, authentication, admin panel, and more. This reduces the amount of time you spend on boilerplate code and lets you focus on building your application.
  2. Scalability: Django is designed to handle high-traffic sites and can scale efficiently as your project grows. It is used by some of the largest websites in the world, demonstrating its scalability and robustness.
  3. Security: Django provides strong security features out of the box, protecting your application from common security threats like SQL injection, cross-site scripting (XSS), and cross-site request forgery (CSRF).
  4. Community and Documentation: Django has a large and active community. The extensive documentation and numerous tutorials available make it easier for developers to learn and get support.
  5. Modularity: Django's modular structure allows you to use only the components you need. This makes it flexible enough to adapt to various project requirements.

By combining Django with Strawberry, you leverage the strengths of both frameworks to build a robust, scalable, and flexible API. This integration allows you to take advantage of Django's mature ecosystem while using GraphQL's efficiency and flexibility to handle data queries.

What Are We Building?

In this tutorial, we'll build a simple book catalog application using Django and Strawberry. This application will allow users to manage and query a collection of books, including details such as title, author, and published date.

Here's a brief overview of what we'll cover:

  • Model Definition: We'll define a Book model with fields for title, author, and published_date.
  • GraphQL Schema: Using Strawberry, we'll create a GraphQL schema to expose our book data. The schema will include:
    • A Query type to fetch the list of books.
    • A Mutation type to add new books to the catalog.
  • GraphQL Queries and Mutations: We'll implement queries to fetch book details and mutations to add new books.
  • Testing: We'll write tests to ensure our GraphQL queries and mutations work as expected using pytest.

By the end of this tutorial, you'll have a functional GraphQL API powered by Django, capable of managing a collection of books with precision and efficiency.

Initial Setup

The very first step is to set up a Python Environment and install Django.

Create a new project folder, create and activate a virtual environment, and install Django:

$ mkdir strawberry_tut && cd strawberry_tut
$ python3 -m venv .env
$ source .env/bin/activate
(.env)$ pip install django==5.0.6

Feel free to swap out virtualenv and Pip for Poetry or Pipenv. For more, review Modern Python Environments.

Bootstrap a new django project:

(.env)$ django-admin startproject strawberry_django_tut
(.env)$ cd strawberry_django_tut

Create a django app:

(.env)$ python manage.py startapp book

To confirm that everything works as it should, start the Django server:

(.env) python manage.py runserver

Navigate to http://127.0.0.1:8000/ to ensure the Django welcome homepage is displayed.

Django Model

Let's define our book model with the appropriate fields. In the strawberry_django_tut/book/models.py file, add the following code

from django.db import models


class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.CharField(max_length=100)
    published_date = models.DateField()

Add the book app to the INSTALLED_APPS list in strawberry_django_tut/strawberry_django_tut/settings.py:

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    "book",
]

Create and apply the migrations:

(.env)$ python manage.py makemigrations
(.env)$ python manage.py migrate

Integrating Strawberry

To add Strawberry to our application, start by installing strawberry-django:

(.env)$ pip install strawberry-graphql-django==0.44.2

Once installed, add strawberry_django to list of INSTALLED_APPS in the settings file.

Then, create a new file called strawberry_django_tut/book/schema.py:

from typing import List

import strawberry
import strawberry_django
from strawberry import auto

from .models import Book


@strawberry_django.type(Book)
class BookType:
    id: auto
    title: auto
    author: auto
    published_date: auto


@strawberry.type
class Query:
    books: List[BookType] = strawberry_django.field()


schema = strawberry.Schema(query=Query)

Notes:

  1. We converted our Book model over to a Strawberry type, mapping the model fields to GraphQL fields, with the @strawberry_django.type decorator. The auto utility provided by Strawberry is used to automatically infer the field types, ensuring consistency between the Django model and the GraphQL schema.
  2. The Query class defines the root query type for our GraphQL API. The Query type is used to define which queries clients can run against our data. In this case, it only has a field books which when queried would return a list of BookType objects. Since we have mapped BookType type to the Book model, querying books would automatically query all Book objects from the database.

Create another file called urls.py in "strawberry_django_tut/book":

from django.urls import path
from strawberry.django.views import GraphQLView

from .schema import schema


urlpatterns = [
    path("graphql/", GraphQLView.as_view(schema=schema), name="graphql"),
]

Finally, update the URL configuration in strawberry_django_tut/strawberry_django_tut/urls.py:

from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("book.urls")),
]

Adding Mutations

In addition to querying data, GraphQL allows you to modify data on the server through mutations. Strawberry handles input by allowing you to define input types, which specify the structure of the data that can be sent to your GraphQL mutations. Input types in Strawberry are created using the @strawberry_django.input decorator for Django models or @strawberry.input for custom input types. In our book catalog application, we'll add a mutation to enable users to add new books to the catalog.

In our schema.py file, let's define a BookInput type, which will correspond to the Book model. It would include the fields title, author and published_date:

@strawberry_django.input(Book)
class BookInput:
    title: auto
    author: auto
    published_date: auto

If you need more control over the input type or if it's not directly tied to a Django model, you can define the input type using the @strawberry.input decorator:

@strawberry.input
class BookInput:
    title: str
    author: str
    published_date: str

This approach is particularly useful when your input type needs to differ from the model or when you're not using Django models.

Let's make use of the input type in our mutation. The mutation will accept an argument of this input type and use the provided data to perform the necessary actions, such as creating a new book in the database.

The strawberry-django integration gives you options to directly use the model for creating without writing extra code.

In the schema.py file, add the following code:

@strawberry.type
class Mutation:
    create_book: BookType = strawberry_django.mutations.create(BookInput)

Then, add the mutation to strawberry.Schema:

schema = strawberry.Schema(query=Query, mutation=Mutation)

If you would want to update and delete all values in the database, you can use the in-built update and delete mutations provided by strawberry-django.

Add a strawberry-django input with the fields set to optional:

@strawberry_django.input(Book)
class BookUpdateInput:
    title: str | None = None
    author: str | None = None
    published_date: str | None = None

In your Mutation class, add the following method:

@strawberry.mutation
def update_book(self, book_id: int, data: BookUpdateInput) -> BookType:
    try:
        book = Book.objects.get(id=book_id)
        for key, value in asdict(data).items():
            if value is not None:
                setattr(book, key, value)

        book.save()

        return book
    except Book.DoesNotExist:
        raise Exception("Not Found")

Don't forget the import:

from dataclasses import asdict

Let's add the mutation to delete also:

@strawberry.mutation
def delete_book(self, book_id: int) -> bool:
    try:
        book = Book.objects.get(pk=book_id)
        book.delete()

        return True
    except Book.DoesNotExist:
        raise Exception("Not Found")

Why can't we just return the book via the BookType like we did in update_book?

Testing the Setup

Let's test this out in the GraphQL Playground. With the Django development server running, navigate to http://127.0.0.1:8000/graphql/.

Run the following queries and mutations:

mutation CREATE_BOOKS {
  createBook(data: {
    title: "My New Book",
    author: "Oluwole",
    publishedDate: "2024-05-31"
  }) {
    id
    title
  }
}

query GET_BOOKS {
  books {
    id
    title
    author
    publishedDate
  }
}

mutation UPDATE_BOOK {
  updateBook(bookId:1, data: {
    title: "My Biography"
  }){
    id
    title
  }
}

mutation DELETE_BOOK {
  deleteBook(bookId: 1)
}

Error Handling

In our examples above, we handled invalid bookId's by raising an exception. Strawberry gives us a better way to handle errors by returning union types that either represent an error or success response. Your client can then look at the __typename of the result to determine the response.

We can add an error Strawberry type with a field called message to the schema.py:

@strawberry.type
class Error:
    message: str

Let's also add a success Strawberry type with a field called result:

@strawberry.type
class Success:
    result: bool

Next, create a union response for the update and delete mutations:

Response = Annotated[
    Union[BookType, Error],
    strawberry.union("BookResponse")
]

DeleteResponse = Annotated[
    Union[Success, Error],
    strawberry.union("DeleteResponse")
]

Add the imports:

from typing import Annotated, List, Union

Now, update the Mutation class like so:

@strawberry.type
class Mutation:
    create_book: List[BookType] = strawberry_django.mutations.create(BookInput)

    @strawberry.mutation
    def update_book(self, book_id: int, data: BookUpdateInput) -> Response:
        try:
            book = Book.objects.get(id=book_id)
            for key, value in asdict(data).items():
                if value is not None:
                    setattr(book, key, value)

            book.save()

            return book
        except Book.DoesNotExist:
            return Error(message="Not Found")
        except Exception as e:
            return Error(f"An error occurred: {str(e)}")

    @strawberry.mutation
    def delete_book(self, book_id: int) -> DeleteResponse:
        try:
            book = Book.objects.get(pk=book_id)
            book.delete()

            return Success(result=True)
        except Book.DoesNotExist:
            return Error(message="Not Found")
        except Exception as e:
            return Error(f"An error occurred: {str(e)}")

To test this, in the GraphQL Playground, run the following mutations:

mutation UPDATE_BOOK {
  updateBook(bookId: 2, data: {
    title: "Updated Title Book"
  }){
    __typename
    ... on BookType {
      id
      title
    }
    ... on Error {
      message
    }
  }
}

mutation DELETE_BOOK {
  deleteBook(bookId: 2){
    __typename
    ... on Success {
      result
    }
    ... on Error {
      message
    }
  }
}

This makes it cleaner and easier to handle specific errors from our code.

Testing with pytest

In order to write tests, install pytest and pytest-django:

(.env)$ pip install pytest==8.2.2 pytest-django==4.8.0

Next, to configure pytest for your Django project, create a file called pytest.ini in the root of your project:

[pytest]
DJANGO_SETTINGS_MODULE = strawberry_django_tut.settings

Create a folder called "tests" within "strawberry_django_tut", and in that folder create a file called test_graphql.py:

import pytest
from django.test import Client
from django.urls import reverse

from book.models import Book


@pytest.fixture
def client():
    return Client()

@pytest.fixture
def create_books(db):
    Book.objects.create(title="Book 1", author="Author 1", published_date="2023-01-01")
    Book.objects.create(title="Book 2", author="Author 2", published_date="2023-01-02")

Here, we created fixtures to add books to our test database.

Go ahead and add the tests, making sure to review each one:

@pytest.mark.django_db
def test_books_query(client, create_books):
    query = """
    query {
        books {
            title
            author
            publishedDate
        }
    }
    """
    response = client.post(reverse("graphql"), {"query": query}, content_type="application/json")
    assert response.status_code == 200
    data = response.json()["data"]["books"]
    assert len(data) == 2
    assert data[0]["title"] == "Book 1"
    assert data[1]["title"] == "Book 2"


@pytest.mark.django_db
def test_add_book_mutation(client):
    mutation = """
    mutation {
        createBook(data: {title: "New Book", author: "New Author", publishedDate: "2023-05-01"}) {
            title
            author
            publishedDate
        }
    }
    """
    response = client.post(reverse("graphql"), {"query": mutation}, content_type="application/json")
    assert response.status_code == 200
    data = response.json()["data"]["createBook"][0]
    assert data["title"] == "New Book"
    assert data["author"] == "New Author"
    assert data["publishedDate"] == "2023-05-01"
    assert Book.objects.filter(title="New Book").exists()


@pytest.mark.django_db
def test_update_book_mutation(client, create_books):
    book = Book.objects.first()
    mutation = f"""
    mutation {{
        updateBook(bookId: {book.id}, data: {{title: "Updated Book"}}) {{
            __typename
            ... on BookType {{
                title
                author
                publishedDate
            }}
            ... on Error {{
                message
            }}
        }}
    }}
    """
    response = client.post(reverse("graphql"), {"query": mutation}, content_type="application/json")
    assert response.status_code == 200
    data = response.json()["data"]["updateBook"]
    assert data["title"] == "Updated Book"
    assert data["author"] == book.author
    assert data["publishedDate"] == str(book.published_date)
    assert Book.objects.filter(title="Updated Book").exists()
    assert not Book.objects.filter(title="Book 1").exists()
    assert Book.objects.filter(title="Book 2").exists()
    assert Book.objects.count() == 2


@pytest.mark.django_db
def test_delete_book_mutation(client, create_books):
    book = Book.objects.first()
    mutation = f"""
    mutation {{
        deleteBook(bookId: {book.id}) {{
            __typename
            ... on Success {{
                result
            }}
            ... on Error {{
                message
            }}
        }}
    }}
    """
    response = client.post(reverse("graphql"), {"query": mutation}, content_type="application/json")
    assert response.status_code == 200
    data = response.json()["data"]["deleteBook"]
    assert data["result"]
    assert not Book.objects.filter(title=book.title).exists()
    assert Book.objects.count() == 1

So, we added tests to get all books, add a book, and update and delete a book.

Ensure they pass:

(.env)$ python -m pytest

Conclusion

In this tutorial, we explored how to integrate Strawberry with Django to build a robust GraphQL API for a book catalog application. We covered the following key steps:

  1. Setting Up: Installing necessary packages and setting up a Django project with Strawberry.
  2. Defining Models: Creating a Django model for the books.
  3. Creating Schemas: Defining types and queries in the GraphQL schema using Strawberry.
  4. Adding Mutations: Implementing mutations to add and update book entries.
  5. Error Handling: How to better handle errors using strawberry's union.
  6. Testing: Writing tests using pytest to ensure that the GraphQL queries and mutations work as expected.

Strawberry combined with Django offers a seamless way to create a type-safe and efficient GraphQL API. The flexibility of defining schemas and the powerful features of Django make this a compelling combination for modern web development. By following the steps outlined, you can quickly build and test a GraphQL API, making it easier to manage and query your data in a structured manner.

With this foundation, you can extend the functionality of your book catalog application further by adding more models, queries, and mutations. Additionally, you can explore advanced features of Strawberry, such as subscriptions, custom scalars, and middleware to enhance your API capabilities. Happy coding!

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.