GraphQL with Python and Django - Advanced

Jun 11, 2017 14:50 · 1136 words · 6 minute read python django graphql guide

Ok, so you tapped into GraphQL?

You’ve done some basic queries, made few mutations and things are looking good so far.

Or are they?

Where is the authorization? Do they provide form fields validation? Doesn’t seem that there is any decent way to test all of that either…

Sounds familiar?

A couple of months ago, we started a project that is aimed to create a mobile application. Technology stack at that moment was standard:

  • Python & Django
  • PostgreSQL
  • Django Rest Framework as an API provider
The project is quite complex mobile application with several dozens of API endpoints anticipated.

Extremely standard setup these days. After four weeks of development several typical problems arose:

  1. Ok, we need endpoints that request and verify SMS code. Is it GET or POST?
  2. What is the resource name? /api/verification-code/ doesn’t look good;
  3. What if we need to re-send an SMS code? Is it the same endpoint with GET parameters?
  4. What kind of a data structure should it be?

Ugh.. Typical RESTful, huh?

I am sure you are all familiar with these problems when RESTful API doesn’t fit perfectly.

Solve all of our problems all at once

(Actually, no)

Long story short, we decided to try GraphQL. It takes two days to pick it up if you are already familiar with DRF. Here are resources to read assuming similar setup:

  1. Start here http://graphql.org/learn/. Read carefully and finish the entire doc before moving next;
  2. Python implementation http://docs.graphene-python.org/en/latest/
  3. Django implementation http://docs.graphene-python.org/projects/django/en/latest/
  4. An enormous amount of issue reported here https://github.com/graphql-python/graphene-django/issues

Was it worth it? Totally yes!

This blog post assumes that you have already done your homework and familiar with basics of GraphQL and its implementation in Django. Here is what I plan to cover:

  • Basic setup example to graph and Django;
  • Working with models. Input types. Nested nodes and mutations;
  • Authorization;
  • _typename_ resolving;
  • Relations and ENUMS
  • Testing

Getting started

Here is a repository where all the code listed below will be committed. I created a dummy project for test purposes to show typical problems we faced so far and the ways to overcome them. Pre-installed libraries:

At that @413dbaa3 point of time, I assume there is a standard GraphQL doc page generated.

Orders application

Let’s put together a simple model:

class Order(models.Model):
    product_name = models.CharField(max_length=255)
    price = models.DecimalField(max_digits=5, decimal_places=2)

and start with basics. First, you need to decide how your queries will look in general. There are two options you may start constructing your queries: as a plain query or using nodes with a relay. Here are considerations and conclusions I’ve made so far:

Plain Query

Pros:

  • Fewer dependencies on the 3rd party;
  • You are the Boss. You have control over the protocol;
  • Remove and reduce potentially unused parts of the output;
  • No additional data transformation. You output your data “as is”.

Cons:

  • You are the Boss. You have to invent your own protocol within a GraphQL;
  • You have to implement pagination. This is huge. It will take you quite some time to implement equivalent solution on your own;

Nodes with Relay

Pros:

  • Pagination. It provides a nice way to iterate over the query objects, slice them in an elegant way;
  • If you have React.js front-end, it is so much easy to use since protocols match;
  • Nice implementation of nested resources and structures.

Cons:

  • Ugly IDs generated by the realy. You might dislike the fact you can’t read the natural integer-like IDs you used to. The solution to this described below.
  • A little complicated structure with edges and nodes.

In the end of the day, I gave up towards relay because of pagination and node structure. However, my biggest problem was to overcome the ugly IDs that were generated by relay. I can understand the reasoning behind but it just doesn’t work for me. Especially, since we knew that IDs are unique within the object type.

To overcome base64 coded IDs that are used by Relay by default, we’ve come up with our own Node implementation that operates with plain IDs:

class DjangoNode(Node):
    """
    This class removes base64 unique ids and operates with plain Integer IDs
    """

    @classmethod
    def get_node_from_global_id(cls, global_id, context, info, only_type=None):
        node = super().get_node_from_global_id(global_id, context, info,
                                               only_type)
        if node:
            return node

        get_node = getattr(only_type, 'get_node', None)
        if get_node:
            return get_node(global_id, context, info)

    @classmethod
    def to_global_id(cls, type, id):
        return id
 

Inputs

Now, it’s time to create a Mutation. The first thing I was looking for an analog of DRF’s ModelSerializer. E.g. how can I create a simple object without too much fuss? The answer is: you can’t. You must create an Input class that defines all your fields and use it in the Mutation. That is clearly not the way I thought it should work.

Simply put: there is no way to construct inputs from your models. This issue https://github .com/graphql-python/graphene-django/issues/121 is inevitable. As many others, I’ve come up with my own solution which can be found here: @99fed405

Usage:

class OrderInput(DjangoInputObjectType):
    items = List(ItemInput, required=True)

    class Meta:
        model = Order
        exclude_fields = ('type', )

It supports exclude_fields, only_fields, all_not_required to make all fields optional. It also handles duplicated enum types. I hope something like that will be merged to the library’s implementation so that everyone can use the same API.

Now, let’s create an order mutation and query. Here is a complete example @ae93fd90. That code produces two queries:

Order List Query

{
  orders {
    edges {
      node {
        id
        productName
        price
      }
    }
  }
}

Response:

{
  "data": {
    "orders": {
      "edges": [
        {
          "node": {
            "id": "1",
            "productName": "My Product",
            "price": 123.5
          }
        },
        {
          "node": {
            "id": "2",
            "productName": "test1",
            "price": 123
          }
        },
        {
          "node": {
            "id": "3",
            "productName": "My Order 1",
            "price": 123
          }
        }
      ]
    }
  }
}

Mutation Create Order Query

mutation {
  createOrder(order: {productName: "My Order 1", price: 123}){
    order {
      id
      productName
    }
  }
}

Response:

{
  "data": {
    "createOrder": {
      "order": {
        "id": "3",
        "productName": "My Order 1"
      }
    }
  }
}

You still need to implement def mutate(root, args, request, info)​​​ yourself. Note, that this code depends on django-filter in order to provide filtering and sorting.

Validation

  • What are the options to validate client’s input with GraphQL?
  • How to reuse Django’s standard validation mechanisms;
  • Error handling and passing meaningful statuses back to the client.
  • Ok, orders are attached to the user. Authentication?

Authentication with Django-GraphQL

  • Ok, what GraphQL currently provide as an auth mechanism? - Nothing;
  • Does Graphene have something already built-in? - Nope;
  • Does it mean we need to implement everything ourselves? - Not necessarily;
  • Integration with DRF’s authentication mechanisms;
  • Permissions should be managed manually so further work required.

Typenames

  • What is the concept: why should you care?
  • Is there anything implemented in this regard already? Spoiler: No.
  • Possible solutions and examples.

We will notify you about new posts every few weeks