GraphQL with Python and Django - Advanced
Jun 11, 2017 14:50 · 1155 words · 6 minute read
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 like there is any decent way to test all of that either…
Sounds familiar?
A couple of months ago, we started a project that was aimed at creating a mobile application.The Technology stack at that moment was standard:
- Python & Django
- PostgreSQL
- Django Rest Framework as an API provider
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 a similar setup:
- Start here http://graphql.org/learn/. Read carefully and finish the entire doc before moving next;
- Python implementation http://docs.graphene-python.org/en/latest/
- Django implementation http://docs.graphene-python.org/projects/django/en/latest/
- An enormous amount of issue reported here https://github.com/graphql-python/graphene-django/issues
Was it worth it? Totally yes!
Was it worth it? yes! This blog post assumes that you have already done your homework and that you’re familiar with the 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 have faced so far and how you can overcome them. Pre-installed libraries:
- Project bootstrapped with Django cookicutter;
- Graphene installed;
- Django Graphene installed;
- Main schema file lives in config/ directory.
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. Firstly, you need to decide how your queries will look in general. There are two options for how you can start constructing your queries: as a plain query or using nodes with a relay. Here are the 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 an 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.
At the end of the day, I gave up on relay because of pagination and node structure. However, my biggest problem was overcoming the ugly IDs that were generated by relay. I can understand the reasoning behind them but they just didn’t work for me. This was especially so because, we knew that IDs are unique within the object type.
To overcome base64 encoded IDs that are used by Relay as a 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.