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, Cleanclass 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 dateclass 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 graphenefrom graphene_django import DjangoObjectTypeclass UserType(DjangoObjectType): class Meta: model = get_user_model()class MovementType(DjangoObjectType): class Meta: model = Movementclass 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_djangofrom 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 typingimport graphenefrom . import modelsfrom .types import LiftType, MovementTypeclass 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 typingfrom datetime import dateimport strawberryimport strawberry_djangofrom . import modelsfrom .types import LiftType, MovementTypeif typing.TYPE_CHECKING: # pragma: no cover from typing import Any from django.http import HttpRequest, JsonResponse from strawberry.types import Infoclass 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.typeclass 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.typeclass 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 settingsfrom django.urls import pathfrom django.views.decorators.csrf import csrf_exemptfrom graphene_django.views import GraphQLViewfrom .schema import schemaurlpatterns = [ path("graphql/", csrf_exempt( GraphQLView.as_view(graphiql=settings.DEBUG, schema=schema) )),]
In Strawberry:
from django.conf import settingsfrom django.urls import pathfrom strawberry.django.views import GraphQLViewfrom .schema import schemaurlpatterns = [ # 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.