Django x GraphQL: Yay or Nay?

·

6 min read

GraphQL is a query language for your API. A top benefit is that it gives the client complete power over the query responses. This means that you can get or modify your query to only return the data you need. It gives frontend developers more flexibility. GraphQL was invented by Dan Schafer and Nick Schrock of Facebook in 2012 and open-sourced in 2015. To use graphQL in Django, you need to install the package called graphene_django.

Direct steps to setting up a GraphQL project

  • Create a Django app

  • Create your models the usual way you would create typical models

from django.db import models


class Actor(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name

    class Meta:
        ordering = ('name',)


class Movie(models.Model):
    title = models.CharField(max_length=100)
    actors = models.ManyToManyField(Actor)
    year = models.IntegerField()

    def __str__(self):
        return self.title

    class Meta:
        ordering = ('title',)
  • install graphene-django using pip install graphene-django

  • Add graphene_django to your installed apps

  • In your settings.py file, specify the schema file that acts as a single entry point for all the queries and mutation

GRAPHENE = {
    'SCHEMA': 'django_graphql_movies.schema.schema'
}
  • In your project folder, create a schema.py file, it should include two classes, Query and Mutation, and these should extend the Query and Mutation classes you will create in your app. Note that this file is different from the schema.py file to be created inside your app.
import graphene
from .movies.schema import Query, Mutation


class Query(Query, graphene.ObjectType):
    pass


class Mutation(Mutation, graphene.ObjectType):
    pass


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

GraphQL only exposes a single endpoint that allows you to send "POST" requests. Basically, every graphQL request is a "POST" request.

Queries

These allow you to fetch data or get data from the server. This is like a rest API's get request.

Mutations

These are used to write or modify data in the database, i.e it would be like a rest API's post or patch request.

  • Add the 'graphql' URL to the root URL.
from django.contrib import admin
from django.urls import path
from graphene_django.views import GraphQLView
from django_graphql_movies.schema import schema

urlpatterns = [
    path('admin/', admin.site.urls),
    path('graphql/', GraphQLView.as_view(graphiql=True)),
]
  • In your app, create a schema.py file and define the types for your models. Types are basically a declaration of the parameters your queries or mutation should give or receive respectively. These are the query types of our simple movie app.
# Create a GraphQL type for the actor model
class ActorType(DjangoObjectType):
    class Meta:
        model = Actor


# Create a GraphQL type for the movie model
class MovieType(DjangoObjectType):
    class Meta:
        model = Movie

Writing a Query

  • In your app's schema.py file, create a Query class, define/register your different queries using the graphene type you want as a response. Each query should extend a type that would represent its structure, and can also extend other values.
# Create a Query type
class Query(ObjectType):
    actor = graphene.Field(ActorType, id=graphene.Int())
    movie = graphene.Field(MovieType, id=graphene.Int())
    actors = graphene.List(ActorType)
    movies = graphene.List(MovieType)

    def resolve_actor(self, info, **kwargs):
        id = kwargs.get('id')

        if id:
            return Actor.objects.get(pk=id)

        return None

    def resolve_movie(self, info, **kwargs):
        id = kwargs.get('id')

        if id:
            return Movie.objects.get(pk=id)

        return None

    def resolve_actors(self, info, **kwargs):
        return Actor.objects.all()

    def resolve_movies(self, info, **kwargs):
        return Movie.objects.all()
  • Each query class takes as many fields as you want to represent, i.e you can pass arguments to queries, these arguments include filter and search parameters. Each field needs to be resolved with a matching method. The function must begin with the "resolve" keyword. Here is a look:
def resolve_actor(self, info, **kwargs):
        id = kwargs.get('id')

        if id is not None:
            return Actor.objects.get(pk=id)

        return None

    def resolve_movie(self, info, **kwargs):
        id = kwargs.get('id')

        if id is not None:
            return Movie.objects.get(pk=id)

        return None

    def resolve_actors(self, info, **kwargs):
        return Actor.objects.all()

    def resolve_movies(self, info, **kwargs):
        return Movie.objects.all()

To note, in graphQL snake_case_namings are converted to camelCase names.

Writing a Mutation

  • Create the input type, this basically declares the parameters your mutation needs to make a request to your API, i.e the data structure of your mutation requests.
class ActorInput(graphene.InputObjectType):
    id = graphene.ID()
    name = graphene.String()


class MovieInput(graphene.InputObjectType):
    id = graphene.ID()
    title = graphene.String()
    actors = graphene.List(ActorInput)
    year = graphene.Int()
  • For each mutation, create a mutation class where the actual work is done. This class should have a "mutate" function where the functionality of the mutation is written, things such as storing into the database, etc.
# Create mutations for actors
class CreateActor(graphene.Mutation):
    class Arguments:
        input = ActorInput(required=True)

    ok = graphene.Boolean()
    actor = graphene.Field(ActorType)

    @staticmethod
    def mutate(root, info, input=None):
        ok = True
        actor_instance = Actor(name=input.name)
        actor_instance.save()
        return CreateActor(ok=ok, actor=actor_instance)


class UpdateActor(graphene.Mutation):
    class Arguments:
        id = graphene.Int(required=True)
        input = ActorInput(required=True)

    ok = graphene.Boolean()
    actor = graphene.Field(ActorType)

    @staticmethod
    def mutate(root, info, id, input=None):
        ok = False
        actor_instance = Actor.objects.get(pk=id)
        if actor_instance:
            ok = True
            actor_instance.name = input.name
            actor_instance.save()
            return UpdateActor(ok=ok, actor=actor_instance)
        return UpdateActor(ok=ok, actor=None)


# Create mutations for movies
class CreateMovie(graphene.Mutation):
    class Arguments:
        input = MovieInput(required=True)

    ok = graphene.Boolean()
    movie = graphene.Field(MovieType)

    @staticmethod
    def mutate(root, info, input=None):
        ok = True
        actors = []
        for actor_input in input.actors:
          actor = Actor.objects.get(pk=actor_input.id)
          if actor is None:
            return CreateMovie(ok=False, movie=None)
          actors.append(actor)
        movie_instance = Movie(
          title=input.title,
          year=input.year
          )
        movie_instance.save()
        movie_instance.actors.set(actors)
        return CreateMovie(ok=ok, movie=movie_instance)


class UpdateMovie(graphene.Mutation):
    class Arguments:
        id = graphene.Int(required=True)
        input = MovieInput(required=True)

    ok = graphene.Boolean()
    movie = graphene.Field(MovieType)

    @staticmethod
    def mutate(root, info, id, input=None):
        ok = False
        movie_instance = Movie.objects.get(pk=id)
        if movie_instance:
            ok = True
            actors = []
            for actor_input in input.actors:
              actor = Actor.objects.get(pk=actor_input.id)
              if actor is None:
                return UpdateMovie(ok=False, movie=None)
              actors.append(actor)
            movie_instance.title=input.title
            movie_instance.year=input.year
            movie_instance.save()
            movie_instance.actors.set(actors)
            return UpdateMovie(ok=ok, movie=movie_instance)
        return UpdateMovie(ok=ok, movie=None)
  • Register all mutations under the general Mutation class, and register and set your schema on graphene.Schema
class Mutation(graphene.ObjectType):
    create_actor = CreateActor.Field()
    update_actor = UpdateActor.Field()
    create_movie = CreateMovie.Field()
    update_movie = UpdateMovie.Field()


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

And your graphql application is good to go.

Testing your app

Like I mentioned earlier, there is only one entry point for a graphql app, "/graphql/. Because my app is not host, I will be using the localhost test URL.

http://127.0.0.1:8000/graphql/

Screen Shot 2021-08-18 at 2.36.56 PM.png

To test your API, you can run your URL on the normal browser, or use API client apps such as Postman and Altair.

Make a Mutation: Create an Actor

Screen Shot 2021-08-18 at 3.16.18 PM.png

Make a Query: Get all Actors

Screen Shot 2021-08-18 at 3.19.23 PM.png

Make a Query: Get Single Actor by ID

Screen Shot 2021-08-18 at 3.30.23 PM.png

Again, The dexterity behind GraphQL that allows clients the ability to craft and demand only specific data from endpoints will always be a plus over REST. So Django x Graphql, Yay or Nay? I look forward to your responses, Thank You.