Adding GraphQL to a Django App
Adding a GraphQL endpoint to your Django application is a simple and powerful way to reduce API maintenance and give more power to your frontend developers.
There are a variety of choices when it comes to libraries to assist you in adding GraphQL support to your application. The two main contenders are Graphene and Strawberry, both of which having specific Django extensions (graphene-django and strawberry-graphql-django) that use introspection on your models in building out the types for the API.
Graphene vs. Strawberry
Graphene has been around for a while and works well. There does seem to be a maintenance issue as it has been awhile since a new release has landed though there is recent activity in the repo.
Strawberry on the other hand is a new comer with active development and releases and has commercial support. I find the API more pleasant in Strawberry as well though my experience with it is still too limited to have a strong opinion on which is best.
With both libraries you build up a set of Types that you want to access through Queries and manipulate through Mutations. Therefore, let's just do a quick side by side comparison of how you build up a GraphQL service for the following data model:
# A specific weightlifting movement, e.g. Bench Press, Squat, Clean
class Movement(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
# Maximum reps of a weight of a Movement on a certain date
class Lift(models.Model):
lifter = models.ForeignKey(User, on_delete=models.CASCADE)
movement = models.ForeignKey(Movement, on_delete=models.CASCADE)
reps = models.IntegerField()
pounds = models.IntegerField()
lifted_on = models.DateField()
created_at = models.DateTimeField(default=timezone.now)
def calculate_max(self, reps: int) -> int:
...
Types
With Graphene we'd have:
import graphene
from graphene_django import DjangoObjectType
class UserType(DjangoObjectType):
class Meta:
model = get_user_model()
class MovementType(DjangoObjectType):
class Meta:
model = Movement
class LiftType(DjangoObjectType):
one_rep_max = graphene.Int()
class Meta:
model = Lift
def resolve_one_rep_max(self, info) -> int:
return self.calculate_max(1)
And with Strawberry there is:
import strawberry_django
from strawberry_django import
@strawberry_django.type(get_user_model())
class UserType:
username: auto
email: auto
@strawberry_django.type(Movement)
class MovementType:
id: auto
name: auto
slug: auto
@strawberry_django.type(Lift)
class LiftType:
id: auto
movement: MovementType
reps: auto
pounds: auto
lifted_on: auto
created_at: auto
@strawberry_django.field
def one_rep_max(self) -> int:
return self.calculate_max(1)
Queries and Mutations
With both Graphene and Strawberry you are building up a Schema object that consists of a Query and/or Mutation object that they themselves contain a set of methods that resolve into Types previously defined.
Building our our types, let's define some queries that allows us to query for Lifts as well as Movements and mutations that allow us to create, update and delete Lifts.
First in Graphene we'd have:
import typing
import graphene
from . import models
from .types import LiftType, MovementType
class Query(graphene.ObjectType):
movements = graphene.List(MovementType)
lifts = graphene.List(LiftType)
def resolve_lifts(self, info, *args, **kwargs) -> list[LiftType]:
return LiftType.get_queryset(
models.Lift.objects.filter(lifter=info.context.user),
info
)
class LiftCreateMutation(graphene.Mutation):
class Arguments:
reps = graphene.Int(required=True)
pounds = graphene.Int(required=True)
lifted_on: = graphene.Date(required=True)
movement_slug = graphene.String(required=True)
ok = graphene.Boolean()
lift = graphene.Field(LiftType)
error = graphene.String()
@classmethod
def mutate(
cls,
root,
info,
reps: int,
pounds: int,
lifted_on: str,
movement_slug: str,
**kwargs
) -> "LiftCreateMutation":
movement = models.Movement.objects.filter(slug=movement_slug).first()
if movement is None:
raise cls(ok=False, error="Movement doesn't exist.")
lift = movement.lifts.create(
lifter=info.context.user,
reps=reps,
pounds=pounds,
lifted_on=lifted_on
)
return cls(ok=True, lift=lift)
class LiftUpdateMutation(graphene.Mutation):
class Arguments:
id = graphene.Int(required=True)
reps = graphene.Int(required=True)
pounds = graphene.Int(required=True)
lifted_on: = graphene.Date(required=True)
movement_slug = graphene.String(required=True)
ok = graphene.Boolean()
lift = graphene.Field(LiftType)
error = graphene.String()
@classmethod
def mutate(
cls,
root,
info,
reps: int,
pounds: int,
lifted_on: str,
movement_slug: str,
**kwargs
) -> "LiftUpdateMutation":
lift = models.Lift.objects.filter(
lifter=info.context.user,
id=id
).first()
if lift is None:
return cls(ok=False, error="Lift doesn't exist.")
movement = models.Movement.objects.filter(slug=movement_slug).first()
if movement is None:
return cls(ok=False, error="Movement doesn't exist.")
lift.movement = movement
lift.reps = reps
lift.pounds = pounds
lift.lifted_on = lifted_on
lift.save(update_fields=["movement", "reps", "pounds", "lifted_on"])
return cls(ok=True, lift=lift)
class LiftDeleteMutation(graphene.Mutation):
class Arguments:
id = graphene.Int(required=True)
ok = graphene.Boolean()
error = graphene.String()
@classmethod
def mutate(
cls,
root,
info,
reps: int,
pounds: int,
lifted_on: str,
movement_slug: str,
**kwargs
) -> "LiftDeleteMutation":
lift = models.Lift.objects.filter(
lifter=info.context.user,
id=id
).first()
if lift is None:
raise cls(ok=False, error="Lift doesn't exist.")
lift.delete()
return cls(ok=True)
class Mutation(graphene.ObjectType):
lift_create = LiftCreateMutation.Field()
lift_update = LiftUpdateMutation.Field()
lift_delete = LiftDeleteMutation.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
And in Strawberry it would look like:
import typing
from datetime import date
import strawberry
import strawberry_django
from . import models
from .types import LiftType, MovementType
if typing.TYPE_CHECKING: # pragma: no cover
from typing import Any
from django.http import HttpRequest, JsonResponse
from strawberry.types import Info
class IsAuthenticated(strawberry.BasePermission):
message = "User is not authenticated"
def has_permission(self, source: "Any", info: "Info", **kwargs) -> bool:
request: "HttpRequest" = info.context["request"]
return request.user.is_authenticated
@strawberry.type
class Query:
movements: list[MovementType] = strawberry_django.field()
@strawberry.field(permission_classes=[IsAuthenticated])
def lifts(self, info: "Info") -> list[LiftType]:
return list(
models.Lift.objects.filter(lifter=info.context.request.user)
)
@strawberry.type
class Mutation:
@strawberry.mutation(permission_classes=[IsAuthenticated])
def lift_create(
self,
reps: int,
pounds: int,
lifted_on: date,
movement_slug: str,
info: "Info",
) -> LiftType:
movement = models.Movement.objects.filter(slug=movement_slug).first()
if movement is None:
raise Exception("Movement doesn't exist.")
return movement.lifts.create(
lifter=info.context.request.user,
reps=reps,
pounds=pounds,
lifted_on=lifted_on
)
@strawberry.mutation(permission_classes=[IsAuthenticated])
def lift_update(
self,
id: int,
reps: int,
pounds: int,
lifted_on: date,
movement_slug: str,
info: "Info",
) -> LiftType:
lift = models.Lift.objects.filter(
lifter=info.context.request.user,
id=id
).first()
if lift is None:
raise Exception("Lift doesn't exist.")
movement = models.Movement.objects.filter(slug=movement_slug).first()
if movement is None:
raise Exception("Movement doesn't exist.")
lift.movement = movement
lift.reps = reps
lift.pounds = pounds
lift.lifted_on = lifted_on
lift.save(update_fields=["movement", "reps", "pounds", "lifted_on"])
return lift
@strawberry.mutation(permission_classes=[IsAuthenticated])
def lift_delete(self, id: int, info: "Info") -> str:
lift = models.Lift.objects.filter(
lifter=info.context.request.user,
id=id
).first()
if lift is None:
raise Exception("Lift doesn't exist.")
lift.delete()
return "ok"
schema = strawberry.Schema(query=Query, mutation=Mutation)
Exposing the Endpoint
Now that we have our schema instances we need to expose them the web. Both
Graphene and Strawberry pretty much do the same thing here, passing the schema
object into a prebuilt Django view that handles all the nitty-gritty of GraphQL
requests and respones.
In Graphene:
from django.conf import settings
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView
from .schema import schema
urlpatterns = [
path("graphql/", csrf_exempt(
GraphQLView.as_view(graphiql=settings.DEBUG, schema=schema)
)),
]
In Strawberry:
from django.conf import settings
from django.urls import path
from strawberry.django.views import GraphQLView
from .schema import schema
urlpatterns = [
# Strawberry's GraphQLView pre-bakes the csrf_exempt decorator so you
# do not need to add it like we do for Graphene
path("graphql/", GraphQLView.as_view(
graphiql=settings.DEBUG,
schema=schema,
))
]
What do you think? Are you building anything with GraphQL in Django? I'd love to hear more about what you doing and if it's different than either of these approaches.
You can find me on Twitter.