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:
- 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.
- 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.
- 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.
- 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.
- 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:
- 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.
- 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.
- 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).
- 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.
- 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 fortitle
,author
, andpublished_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.
- A
- 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:
- We converted our
Book
model over to a Strawberry type, mapping the model fields to GraphQL fields, with the@strawberry_django.type
decorator. Theauto
utility provided by Strawberry is used to automatically infer the field types, ensuring consistency between the Django model and the GraphQL schema. - The
Query
class defines the root query type for our GraphQL API. TheQuery
type is used to define which queries clients can run against our data. In this case, it only has a fieldbooks
which when queried would return a list ofBookType
objects. Since we have mappedBookType
type to theBook
model, queryingbooks
would automatically query allBook
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 inupdate_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:
- Setting Up: Installing necessary packages and setting up a Django project with Strawberry.
- Defining Models: Creating a Django model for the books.
- Creating Schemas: Defining types and queries in the GraphQL schema using Strawberry.
- Adding Mutations: Implementing mutations to add and update book entries.
- Error Handling: How to better handle errors using strawberry's union.
- 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!