Django + Graphene: From REST to GraphQL

Written by

This article dives into using Graphene with Django and is structured in three parts: Intro to the Basics; Testing GraphQL and REST endpoints in Django; and Authorization, Pagination, and Filtering.

There are a lot of misconceptions about using Django and Graphene. For example, many people believe if you want to use GraphQL you need to migrate your whole Django backend to Node. The truth is not only can you use GraphQL on top of your current Django app but there are ways to improve the whole process.

The following is a three-part article on using Graphene with Django:

  1. Django + Graphene: Intro the basics.
  2. Django + Graphene: Testing GraphQL and REST endpoints in Django.
  3. Django + Graphene: Authorization, Pagination, and Filtering

Django + Graphene: Intro to the Basics

I want to use GraphQL but...

For many developers, GraphQL seems intimately tied to JavaScript. For example, most GraphQL tutorials and guides require you to set up an ExpressJS server to start consuming endpoints. Of course, we can do it like that, but it’s a lot of unnecessary work. The reality is GraphQL is not a JavaScript-only library, but a query language that lets you get the data that you requested. The GraphQL-Express integration is the most popular but integrations exist for many different languages. For example:

Java: https://github.com/graphql-java/graphql-java

PHP: https://github.com/webonyx/graphql-php

Python: https://graphene-python.org/

Ruby: https://graphql-ruby.org/

Enter Graphene...

I’ve always loved Django. It was the first framework I worked with and helped me a lot in FullStack Labs. When GraphQL came out, I was blown away and wondered how I could integrate it with Django. You can start consuming Django REST endpoints under an express server, but it just doesn’t feel right having two servers consuming so many resources. In order to be more flexible with my data responses, I decided to try Graphene.

Graphene is a powerful library that provides an extendable API, making it easier to integrate your current codebase. You could use graphene-python with your stack right out of the box. But today we will be working with its Django integration, which will give us some additional abstractions to work with.

Getting Started

To install the library in your project, type:

pip install graphene-django

Note: in our requirements.txt we must set the version number the library has, to avoid introducing weird bugs with future versions. 

Next, we create a new app where we are going to set up our graph and proceed to enable these apps in our current project.

django-admin startapp graph
INSTALLED_APPS = [
    ...
    'django.contrib.staticfiles', # Required for GraphiQL
    'graphene_django',
    'graph'
]

Once we get this done, let’s enable our route to the graphQL endpoint in the main app urls.py. (You can disable the graphiql client by setting it with False, but we’ll be using it in this example.) 

# urls.py

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

urlpatterns = [
    # ...
    path("graphql", GraphQLView.as_view(graphiql=True)),
]

Let’s add the path to our schema in our settings.py:

GRAPHENE = {
  'SCHEMA': 'graph.schema.schema'}

Even though we’ve set up our path, we’re still not ready to start using our GraphQL endpoint. How do we set it? Let’s go over the basics! 

Schema

Graphene provides us with a schema object attached with resolve methods to fetch data. In Django, we are accustomed to creating apps according to functionality; this structure allows us to create more order and readability in our code. In our demo repository, we have already set up two apps, players and games. So, for these apps, we need to create a schema.py file.

The [player|games]/schema.py should look like this:

import graphene

class Query(graphene.ObjectType):
	pass

class Mutation(graphene.ObjectType):
	pass

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

We see that these classes are receiving an ObjectType, creating the relationship between the fields in the schema and telling us how to deliver data.

Now let’s register these schemas into our graph/schema.py, so we can pass as many Query and Mutation objects as possible.

# graph/schema.py

import player.schema
import games.schema
import graphene

class Query(player.schema.Query, games.schema.Query, graphene.ObjectType):
    pass

class Mutation(player.schema.Mutation, games.schema.Mutation, graphene.ObjectType):
    pass


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

So far this schema does nothing, but we’re ready to start adding queries and mutations related to your apps. Let’s breathe some life into it!

Types

We previously talked about how graphene-django gives us some abstractions to start working with immediately, and one of these is DjangoObjectType. This allows us to convert our Django model into an object type right out of the box, so let’s create a file called types.py.

The player/types.py should look like this:

# player/types.py

from graphene_django.types import DjangoObjectType
from django.contrib.auth.models import User
from .models import Player, PlayerBoard

class UserType(DjangoObjectType):
	class Meta:
    	model = User
      exclude = ('password')

class PlayerType(DjangoObjectType):
	class Meta:
    	model = Player
    	fields = ('user', 'picture', 'gender', 'birthday', 'motto', 'board')

class PlayerBoardType(DjangoObjectType):
	class Meta:
    	model = PlayerBoard
    	fields = '__all__'

The DjangoObjectType by default includes all the fields in the model, but we can explicitly add or exclude any fields we want. The fields attribute receives either a dictionary with the fields or the '__all__' string to include all the fields in the model.

To include the relations in the query, you have to include the field name and define that model as a DjangoObjectType subclass. If you ever find yourself wondering why your entities are failing when querying, first check that you have defined the model as DjangoObjectType.

Queries

We have seen how to define our models as types, but how do we map the query to the data? 

Every time the schema receives a query, it is mapped to a resolver using this pattern:

def resolve_foo(self, info, **kwargs):

foo is the field we’ve defined in the query object, so let’s implement this in our app.

Now go back to player/schema.py and update it with the queries we need:

# player/schema.py

import graphene

from .models import Player, PlayerBoard
from .types import PlayerType, PlayerBoardType

class Query(graphene.ObjectType):
	players = graphene.List(PlayerType)
	player = graphene.Field(PlayerType, player_id=graphene.Int())
	player_board = graphene.List(PlayerBoardType)

	def resolve_players(self, info, **kwargs):
    	    return Player.objects.all().order_by('-board__points')

	def resolve_player(self, info, player_id):
    	    return Player.objects.get(pk=player_id)

	def resolve_player_board(self, info, **kwargs):
    	    return PlayerBoard.objects.all().order_by('-points')

schema = graphene.Schema(query=Query)

Here we can see a couple of interesting things: we enable the queries and map the resolving data. We also have a simple graphene.List. For this, we’re assuming that we won't be receiving any parameters to filter, we just need a list of data. Meanwhile, the graphene.Field makes sure to enable extra fields in the schema.

Now, just like that, our app is available to respond to graphQL queries. Here we are using the logic directly, but you can include your existing classes to handle the listing of your entities.

Mutations

I wasn’t joking when I mentioned that graphene-django provides us with some powerful abstractions. And while we’re using the graphene mutations to include changes to the data, we can use the CRUD feature that Django offers.

As an example, let’s create a mutations.py file like this:

# player/mutations.py

import graphene

from .types import PlayerType
from .models import Player

class EditPlayerMutation(graphene.Mutation):
	class Arguments:
    	    # The input arguments for this mutation
    	    id = graphene.ID()
    	    picture = graphene.String()
    	    gender = graphene.String()
    	    birthday = graphene.String()
    	    motto = graphene.String()

	# The class attributes define the response of the mutation
	player = graphene.Field(PlayerType)

	def mutate(self, info, id, picture, gender, birthday, motto):
    	    
          player = Player.objects.get(pk=id)
    	    player.picture = picture
    	    player.gender = gender
    	    player.birthday = birthday
    	    player.motto = motto
    	    player.save()
    	    # Notice we return an instance of this mutation
    	    return EditPlayerMutation(player=player)

In our code, we declare the required arguments to mutate the data. Next, we declare the response from the mutation as we would do with the queries. Finally, we include the logic for the mutation.

Let's wire our mutation into our app schema.py, which should look like this:

# player/schema.py

import graphene

from .models import Player, PlayerBoard
from .types import PlayerType, PlayerBoardType
from .mutations import EditPlayerMutation

class Query(graphene.ObjectType):
	players = graphene.List(PlayerType)
	player = graphene.Field(PlayerType, player_id=graphene.Int())
	player_board = graphene.List(PlayerBoardType)

	def resolve_players(self, info, **kwargs):
    	    return Player.objects.all().order_by('-board__points')

	def resolve_player(self, info, player_id):
    	    return Player.objects.get(pk=player_id)

	def resolve_player_board(self, info, **kwargs):
    	    return PlayerBoard.objects.all().order_by('-points')

class Mutation(graphene.ObjectType):
	update_player = graphene.Field(EditPlayerMutation)

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

We could use the default Django CRUD to handle the mutations, but moving all your logic to this standard seems like a lot of work. Luckily, graphene-Django allows us to use our previously created Django Forms or Django REST Serializers. Let’s see how to do this with Django REST; it’s much easier.

Our mutation file should look like this:

# player/mutations.py

import graphene
from graphene_django.rest_framework.mutation import SerializerMutation

from .types import PlayerType
from .models import Player
from .serializers import PlayerSerializer

class CreatePlayerMutation(SerializerMutation):
	class Meta:
    	    serializer_class = PlayerSerializer

class EditPlayerMutation(graphene.Mutation):
	class Arguments:
    	    # The input arguments for this mutation
    	    id = graphene.ID()
    	    picture = graphene.String()
    	    gender = graphene.String()
    	    birthday = graphene.String()
    	    motto = graphene.String()

	# The class attributes define the response of the mutation
	player = graphene.Field(PlayerType)

	def mutate(self, info, id, picture, gender, birthday, motto):
    	    player = Player.objects.get(pk=id)
    	    player.picture = picture
    	    player.gender = gender
    	    player.birthday = birthday
    	    player.motto = motto
    	    player.save()
    	    # Notice we return an instance of this mutation
    	    return EditPlayerMutation(player=player)

Now let’s update our player/schema.py:

import graphene

from .models import Player, PlayerBoard
from .types import PlayerType, PlayerBoardType
from .mutations import CreatePlayerMutation, EditPlayerMutation

class Query(graphene.ObjectType):
	players = graphene.List(PlayerType)
	player = graphene.Field(PlayerType, player_id=graphene.Int())
	player_board = graphene.List(PlayerBoardType)

	def resolve_players(self, info, **kwargs):
    	    return Player.objects.all().order_by('-board__points')

	def resolve_player(self, info, player_id):
    	    return Player.objects.get(pk=player_id)

	def resolve_player_board(self, info, **kwargs):
    	    return PlayerBoard.objects.all().order_by('-points')

class Mutation(graphene.ObjectType):
	create_player = graphene.Field(CreatePlayerMutation)
	update_player = graphene.Field(EditPlayerMutation)

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

And just like that, we have included our serializer without duplicating any code! The default integration with Django REST enables creations and updates into the model and gives you the ability to override the update queries.

Django + Graphene: Testing GraphQL and REST Endpoints in Django

Test coverage

In our starter project, we have a REST API with no coverage—our main goal is to make it better.

The first step is to know the status of our app; we have a library that will easily show us. So let’s install it!

pip install coverage

In order to check the coverage, we execute the following command:

coverage run -source='.' manage.py test
coverage report

So far, this project has no testing, which is not good.

Fake Database

I believe tests should be isolated from the app's data layer. This gives us more speed and shows that our code follows a decoupled pattern; it has the added benefit of putting us in the right mindset to create clean and maintainable code.

We will be using a library called mixer that will allow us to blend our models with fake data, making it easier and faster to run our tests. 

pip install mixer

To include mixer we need to import it into our player/test.py:

from mixer.backend.django import mixer

Queries and Mutations

Now that we have installed and imported our fake library, we need to set up the queries and mutations we will be testing. In our example we'll be testing the endpoint to get the list of players. So our query string might look like this:

PLAYERS_QUERY = '''
 {
   players {
     user {
       id
       username
       firstName
       lastName
       email
       isActive
     }
     picture
     gender
     motto
     birthday
   }
 }
'''

And for our mutations, we will be using as an example the update single-player mutation, so our string might look like this:

UPDATE_PLAYER_MUTATION = '''
 mutation editPlayerMutation($id: string, $motto: string) {
     updatePlayer(id: $id, motto: $motto) {
         user {
           id
           username
           firstName
           lastName
           email
           isActive
         }
         picture
         gender
         motto
         birthday
     }
 }

Testing GraphQL

Unit Testing

Unit testing is an approach that will test each part of our application in isolation, allowing us to capture bugs in earlier stages of the development process.

After we have installed the library, we’ll start creating the unit test for our endpoints. Graphane-Django includes some utilities to make it painless; in order to start using it, our test class must inherit from the GraphQLTestCase class, so let’s import it in our tests file:

players/tests/tests_unit.py

from graphene_django.utils.testing import GraphQLTestCase
from player.schema import schema

Let’s begin the test:

class PlayerUnitTestCase(GraphQLTestCase):
 GRAPHQL_SCHEMA = schema
 def setUp(self):

   self.player1 = mixer.blend(Player)
   self.player2 = mixer.blend(Player)

 def test_players_response(self):

   response = self.query(
     PLAYERS_QUERY,
     op_name='players'
   )

   content = json.loads(response.content)

   self.assertResponseNoErrors(response)
   assert len(content['data']['players']) == 2

In the GraphQLTestCase class, we need to set an attribute called GRAPHQL_SCHEMA. Here is where we execute the query to get the players and start making assertions according to the simulated data.

We have tested the query, but we should also test the mutation:

def test_update_player_response(self):

   response = self.query(
     UPDATE_PLAYER_MUTATION,
     op_name='editPlayerMutation',
     variables={'id': self.player1.user.id, 'motto': 'YOLO'}
   )

   self.assertResponseNoErrors(response)

For our input, we pass the variables attribute with the values you want to replace in the mutation.

As you can see, it’s fairly easy to start testing our GraphQL endpoints with the Django-Graphene integration. But as our app starts to grow, we sometimes find ourselves testing the same response over and over again. To make our lives a little easier, we might use snapshot testing.

Snapshot Testing

When we make changes to our API, we might introduce bugs that could break our app. We can create multiple utilities, but the most effective way to view what we’re introducing in the code is using snapshots as it will show every change we make.

In order to create snapshots, we need to install the snapshottest library:

pip install snapshottest

Next, we need to change the TEST_RUNNER to use snapshottest in our settings.py file:

TEST_RUNNER = 'snapshottest.django.TestRunner'

And then we import our libraries in our snapshot tests file.

players/tests/tests_snapshot.py

from snapshottest import TestCase
from graphene.test import Client

Here we’re loading the Graphene client to integrate with the snapshottest library. The client is a dummy GraphQL client that will let us simulate queries and mutations to get the response. 

Note: We’ll be using the integration with unittest to generate the snapshots.

class PlayerSnapshotCase(TestCase):

 def setUp(self):
   self.client = Client(schema)
   self.player1 = mixer.blend(Player)
   self.player2 = mixer.blend(Player)

 def test_api_players_snapshot(self):

   """Testing the API for /players"""
   response = self.client.execute(PLAYERS_QUERY)
   self.assertMatchSnapshot(response)

As you can see, it’s easy to generate a snapshot. In the setUp function we create a client passing the schema we are currently testing. In our test case we are faking two players in order to populate the values that will be included in the snapshot. The snapshot will be created when the test is executed for the first time. After that it will run a diff checker to validate any change. We can also test a mutation:

def test_api_update_player_snapshot(self):

   """Testing the API for /players"""
   response = self.client.execute(UPDATE_PLAYER_MUTATION, variables={'id': self.player1.user.id, 'motto': 'YOLO'})
   self.assertMatchSnapshot(response)

The only difference is that we pass an attribute called variables with the values required for the mutation.

As you can see, it’s pretty easy to start testing our code using Django with Graphene; most utilities come ready for use. Note that snapshot testing will not replace unit testing. The snapshots provide visibility to changes, while the unit test makes sure our application is working properly after they are made.

Testing Django Rest

What about when our project is already created and running but our code has no tests? We need to create a strategy to determine which things we should test and which things we should not. Keep in mind that you should not test what is already tested. What I mean by this is that Django built-in models and views have been already tested. You should test only the code you have customized. That’s why we are relying on our coverage library in order to tell us what parts in our project need to be addressed. So let’s dive in!

Testing our views

Let’s consider the next couple of views as examples. The first example contains a view that only handles a GET and a POST request and our second example contains a view that handles a PUT and a DELETE request. How should we tackle this?

players/views.py

@api_view(['GET', 'POST'])
def player_list(request):

   if request.method == 'GET':
       players = Player.objects.all().order_by('-board__points')
       serializer = PlayerSerializer(players, many=True)
       return Response(serializer.data)
   elif request.method == 'POST':
       serializer = PlayerSerializer(data=request.data)
       if serializer.is_valid():
           serializer.save()
           return Response(serializer.data, status=status.HTTP_201_CREATED)
       return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Inside our tests folder we need to create a test_views.py file that will contain the test for the views we have already created in our project. Let’s import what we need and get started:

import json
from rest_framework import status
from mixer.backend.django import mixer
from django.urls import reverse
from django.test import TestCase, Client
from player.models import Player
from player.serializers import PlayerSerializer

Testing our GET requests

class GetAllPlayersTest(TestCase):
   """ Test module for GET all players API """

 def setUp(self):
   mixer.blend(Player)
   mixer.blend(Player)

 def test_player_list(self):
   # Get Response from the API
   response = client.get(reverse('player_list'))
   # Get the faked data to assert from the DB
   players = Player.objects.all()
   serializer = PlayerSerializer(players, many=True)
   self.assertEqual(response.data, serializer.data)
   self.assertEqual(response.status_code, status.HTTP_200_OK)

What we’re doing here is mocking our players list in the `setUp` of the test. We then call our endpoint that in our urls.py file we have named player_list using a GET request. After that we compare the data queried against the response and validate that the response is correct. This is a basic example of how you can test a GET request. You can keep expanding the assertions to match your needs.

Testing our POST requests

class CreateNewPlayerTest(TestCase):
 """ Test module for inserting a new player """

 def setUp(self):
   self.user_payload = {
     'username': 'new_test',
     'email': 'test@mail.com',
     'first_name': 'Created',
     'last_name': 'Test User',
     'password': 'Uc4ns33me__',
   }
   self.valid_payload = {
     'user': self.user_payload,
     'motto': 'YOLO',
     'gender': 'F',
     'birthday': '1992-12-25'
   }
   self.invalid_payload = {
     'user': self.user_payload,
     'motto': 'NOT YOLO',
     'gender': 'M',
   }

 def test_create_valid_player(self):
   response = client.post(
       reverse('player_list'),
       data=json.dumps(self.valid_payload),
       content_type='application/json'
   )
   self.assertEqual(response.status_code, status.HTTP_201_CREATED)

 def test_create_invalid_player(self):
   response = client.post(
       reverse('player_list'),
       data=json.dumps(self.invalid_payload),
       content_type='application/json'
   )
   self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

Our POST tests follow the same structure from the GET. In the setUp we initialize the payload we’ll be passing as our data. See that we’re currently testing two cases. In the first case we expect an ok response and we pass a valid payload and in the second case we’re testing how it would respond using an invalid payload.

What about PUT and DELETE?

class UpdateSinglePlayerTest(TestCase):
 """ Test module for updating an existing player record """

 def setUp(self):
   self.player1 = mixer.blend(Player)
   self.player2 = mixer.blend(Player)
   self.valid_payload = {
     'motto': 'YOLO',
   }
   self.invalid_payload = {}

 def test_valid_update_player(self):
   response = client.put(
       reverse('single_player', kwargs={'pk': self.player1.pk}),
       data=json.dumps(self.valid_payload),
       content_type='application/json'
   )
   self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

 def test_invalid_update_player(self):
   response = client.put(
       reverse('single_player', kwargs={'pk': self.player2.pk}),
       data=json.dumps(self.invalid_payload),
       content_type='application/json')
   self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)


class DeleteSinglePlayerTest(TestCase):
 """ Test module for deleting an existing player record """

 def setUp(self):
   self.player1 = mixer.blend(Player)

 def test_valid_delete_player(self):
   response = client.delete(
       reverse('single_player', kwargs={'pk': self.player1.pk}))
   self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

 def test_invalid_delete_player(self):
   response = client.delete(
       reverse('single_player', kwargs={'pk': 20}))
   self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

We have seen how to make tests for our GET and POST requests. The same concepts apply for the PUT and DELETE requests. We need to mock a valid entity and then validate the response against a minimum of two scenarios: a valid request and an invalid request. After this we can run our coverage command to see how our app is improving and what other parts we need to address.

Testing Conclusion

We need to create a mentality where testing must come first. The time you spend at the beginning of development planning your code and making it fail-proof will save you future headaches when debugging and implementing new features. Our tests will serve as documentation for the code we’re writing and will give us a better understanding of what needs to be done and how to tackle it.

Django + Graphene: Authorization, Pagination, and Filtering.

In the process of improving our app and making our change into using GraphQL with Django, there are some features that we always have to keep in mind, these are authorization, pagination and filtering. These three features provide us with enough security and speed inside our project, so let’s take a look on how to approach each of these points.

Authorization

One of the most important points when it comes to data security is to know how much data you’re allowing to the client consuming the application, not every user calling the endpoint has the same permissions when fetching for data and our app has to be responsive to those kinds of situations. Let’s see how Graphene helps us to achieve this goal.

We have used GraphQLView in our previous article to define our GraphQL endpoint, when we do this we can access the request and the context arguments, so we can easily handle authorization of resources from the resolve function. Let’s see how we can do this.

Currently our player schema might look like this:

player/schema.py

from graphene_django.filter import DjangoFilterConnectionField

class Query(graphene.ObjectType):
 players = DjangoFilterConnectionField(PlayerType)

 def resolve_players(self, info, **kwargs):
   if not info.context.user.is_authenticated():
     return Player.objects.none()
   else:
     return Player.objects.all().order_by('-board__points')

As we see here, we have updated our query field to use a utility from Django’s Graphene that might help us with the filtering of the field based on the type we pass, this one is called DjangoFilterConnectionField.

In our resolve function we’re using the context provided by django by default when using our GraphQLView.

Graphene can also use Django's LoginRequiredMixin that can help us to redirect the user to a login page before using any of our GraphQL endpoints. Let’s see how to implement this.

In our graph/views.py we might need to create a view that extends from our GraphQLView.

graph/views.py

from django.contrib.auth.mixins import LoginRequiredMixin
from graphene_django.views import GraphQLView

class PrivateGraphQLView(LoginRequiredMixin, GraphQLView):
   pass

Then in our app urls we update our GraphQLView for the one we’ve just created.

pong/urls.py

from graph.views import PrivateGraphQLView
from graph.schema import schema

urlpatterns = [
   #...
   path('graphql/', PrivateGraphQLView.as_view(graphiql=True, schema=schema
)),
   #...
]

As we can see, it’s pretty easy to implement this security measure using Graphene.

Pagination

We have to think about pagination every time we’re dealing with any amount of data. Sometimes it might look like overkill, but loading a huge chunk of information in a single request is neither good for your clients nor for your server.

On Django’s Graphene it’s pretty easy to add pagination to our requests. By default, Graphene uses a Relay module to handle the pagination, but we won’t be using that because it is simpler to integrate pagination within our current query’s resolve function. All we have to do is make a few tweaks.

from graphene_django.filter import DjangoFilterConnectionField

class Query(graphene.ObjectType):
 players = DjangoFilterConnectionField(
   PlayerType,
   first=graphene.Int(),
   skip=graphene.Int()
 )

 def resolve_players(self, info, **kwargs):
   if not info.context.user.is_authenticated():
     return Player.objects.none()
   else:
     qs = Player.objects.all().order_by('-board__points')

     if skip:
       qs = qs[skip:]

     if first:
       qs = qs[:first]

     return qs

As we see, in our query declaration we have added two extra parameters, skip and first, that will allow us to use the Python slice approach where we load the first n values and skip them in order to handle the pagination for the query. Then in our resolve function, we implement our attributes and return the result of the query.

Filtering

Last but not least, filtering provides us with the ability to further clean our query, making it quicker to respond with the exact data we need. Filtering fields with Graphene relies on an external library called Django-Filter, so let’s quickly install it and enable it in our application:

pip install coverage‍

pong/settings.py

INSTALLED_APPS = [
   # ...
   django_filters
   #...
]

After we have enabled our app it is easy to add the filtering to our endpoint. All we need to do is update our create types to include an attribute called filter_fields. In this attribute we can pass simple filters as well as more complex ones. Let’s see some examples displaying both:

player/types.py

class PlayerType(DjangoObjectType):
   class Meta:
       model = Player
       filter_fields = ['gender', 'birthday']
       fields = ('user', 'picture', 'gender', 'birthday', 'motto', 'board')

As we can see, we only pass the field we want to filter by, and then Graphene will handle them by default. 

But sometimes this simple filter won’t be enough. Let’s see how we can create more complex filters for our query.

In order to create our filter, we need to import django_filters into our types files.

player/types.py

import django_filters

And then proceed to create our custom filter.

player/types.py

class PlayerFilter(django_filters.FilterSet):
   # Do case-insensitive lookups on 'motto'
   motto = django_filters.CharFilter(lookup_expr=['iexact'])

   class Meta:
       model = Player
       fields = ['user', 'picture', 'gender', 'birthday', 'motto', 'board']

So far so good. But how do we use this filter in our query? This is fairly easy as well. We need to import the filter in our query file and pass it into our attribute called filterset_class.

players/schema.py

from graphene_django.filter import DjangoFilterConnectionField
from player.types import PlayerFilter

class Query(graphene.ObjectType):
 players = DjangoFilterConnectionField(
   PlayerType,
   filterset_class=PlayerFilter
 )

 def resolve_players(self, info, **kwargs):
   if not info.context.user.is_authenticated():
     return Player.objects.none()
   else:
     Player.objects.all().order_by('-board__points')

Once we have passed our custom filter, our query will use this one to include the validations by django_filters.

Final Thoughts

Graphene is a great library that can help us get our endpoints up and running with relatively little configuration. We don’t need extra instances to run GraphQL with Python and it’s pretty easy to use. The team at FullStack Labs noticed that Graphene has been well received by the community, but there are few resources on the web to help you to address some of the challenges that come with using it. Hopefully, this article has given you some perspective on how to start using Graphene to get your development up and running.

Learn more

Frequently Asked Questions