17 Oct

10 minutes to a custom GraphQL Backend

First the backstory: I have been using GraphQl for react apps for a bit now, and I found myself yearning for an easy to build backend. There are several GraphQL as a service providers popping up, but I don’t like the idea of trusting my data to new comers since I am not sure how long they’ll be around and wether I’ll be able to get my data out should they go out of business.

On to business. Let’s build a GraphQL backend in Python. We’ll need authentication, and some sort of ORM. I plan to host this on AWS Lambda so I also want to support DynamoDB.

Getting started

First we create the directory and setup a virtualenv (I am using Python 2.7 as AWS Lambda doesn’t officially support python 3 (although there are workarounds)

$ mkdir awesome-backend && cd $_ && virtualenv env && . ./env/bin/activate

Then we install a few packages. Well need Flask, Flask-JWT for authentication, Flask-GraphQL and my own package graphene-pynamodb for the GraphQL bits:

$ pip install Flask Flask-Cors Flask-JWT Flask-GraphQL graphene-pynamodb
$ pip freeze > requirements.txt

Creating the backend

Let’s start with app.py. This will be our main Flask entry point. Make sure to generate a (sensible secret key)

Here we setup a few config keys to configure Flask-JWT. We also import our custom authenticate and identity functions (see auth.py further down)

import os
from datetime import timedelta

from flask import Flask
from flask_cors import CORS
from flask_jwt import JWT

from auth import identity, authenticate

app = Flask(__name__)
app.config["DEBUG"] = os.environ.get("DEBUG", True)
app.config["JWT_AUTH_USERNAME_KEY"] = "email"
app.config["JWT_EXPIRATION_DELTA"] = timedelta(7 * 24 * 60 * 60)
app.config["JWT_AUTH_URL_RULE"] = "/login"
app.config["SECRET_KEY"] = "Super duper secret"

CORS(app)
jwt = JWT(app, authenticate, identity)

Adding a user model

Next we need to create a User model to hold our users. An additional consideration here is we will be querying by id most times except on the initial login. So we’ll make our hash_key a uuid4 id, and we’ll add a GlobalSecondaryIndex on email to query by email for login.

We’ll create a models.py file and insert the following code:

import uuid

from pynamodb.attributes import UnicodeAttribute
from pynamodb.indexes import AllProjection
from pynamodb.indexes import GlobalSecondaryIndex
from pynamodb.models import Model
from werkzeug.security import check_password_hash, generate_password_hash


class PasswordAttribute(UnicodeAttribute):
    def serialize(self, value):
        return generate_password_hash(value)

    def deserialize(self, value):
        return value


class UserEmailIndex(GlobalSecondaryIndex):
    class Meta:
        read_capacity_units = 1
        write_capacity_units = 1
        projection = AllProjection()

    email = UnicodeAttribute(hash_key=True)


class User(Model):
    class Meta:
        table_name = "users"

    def __init__(self, hash_key=None, range_key=None, **args):
        Model.__init__(self, hash_key, range_key, **args)
        if not self.id:
            self.id = str(uuid.uuid4())

    id = UnicodeAttribute(hash_key=True)
    email = UnicodeAttribute(null=False)
    email_index = UserEmailIndex()
    first_name = UnicodeAttribute(null=False)
    last_name = UnicodeAttribute(null=False)
    password = PasswordAttribute(null=False)

    def check_password(self, password):
        return check_password_hash(self.password, password)


if not User.exists():
    User.create_table(read_capacity_units=1, write_capacity_units=1, wait=True)

Note: if we create a User without passing id to the constructor, we generate a uuid4 id automatically.

Setting up authentication

Now onto the actual login code. We need to initialize Flask-JWT with two functions, one to check the email/password, the other to convert a token to a user object.

Lets create a new auth.py file and add:

from flask import g

from models import User


def authenticate(email, password):
    try:
        user = User.email_index.query(email).next()
    except StopIteration:
        return None

    if user and user.check_password(password):
        return user


def identity(payload):
    user_id = payload['identity']
    try:
        g.user = User.get(user_id, None)
        return g.user
    except User.DoesNotExist:
        return None

Adding GraphQL to the mix

We need to create an authenticated graphql endpoint. We can also provide a schema endpoint to update our client side schema.json

Let’s do this in a schema.py file. Here we define a User type in GraphQL, we tie it to our PynamoDB Model, and exclude the password field. We also make sure anyone using the node(id:”…”) type queries is limited to the currently logged in user.

In the Query definition, we define two nodes here, a relay node, and a viewer. We resolve the viewer to the logged in user.

To expose our GraphQL schema, we create GraphQLView, make sure it requires authentication, and optionally turn on the graphiql UI if we are running in DEBUG mode.

You can see the graphiql UI by going to your endpoint in a browser and passing the header “Authorization: JWT [The token you got from /login]”. I use the excellent ModHeaders chrome extension for this.

import graphene
from flask import g, jsonify
from flask_graphql import GraphQLView
from flask_jwt import jwt_required
from graphene import relay
from graphene_pynamodb import PynamoObjectType

from app import app
from models import User as UserModel


class User(PynamoObjectType):
    class Meta:
        model = UserModel
        interfaces = (relay.Node,)
        exclude_fields = ['password']

    @classmethod
    def get_node(self, id, context, info):
        try:
            logged_in_user = g.user
        except AttributeError:
            return None

        return logged_in_user


class Query(graphene.ObjectType):
    node = relay.Node.Field()
    viewer = graphene.Field(User, )

    def resolve_viewer(self, args, context, info):
        try:
            logged_in_user = g.user
        except AttributeError:
            return None

        return logged_in_user


schema = graphene.Schema(query=Query)


def graphql_token_view():
    view = GraphQLView.as_view('graphql', schema=schema, graphiql=bool(app.config.get("DEBUG", False)))
    view = jwt_required()(view)
    return view


app.add_url_rule('/graphql', view_func=graphql_token_view())


@app.route("/graphql-schema", methods=['GET'])
def graphql_schema():
    schema_dict = {'data': schema.introspect()}
    return jsonify(schema_dict)

Wrap it up!

And finally, let’s run the whole thing. You can create a separate run.py file here to run our Flask app. You could also put all this in a subdirectory and add a __main__.py. Or you could add if __name__ == “__main__” to an existing file.

from schema import app

app.run()

Using the backend:

And we are done! So how do we use this from our React app? On login, we POST our email and password to the backend’s /login.

An important note here is that Flask-JWT does not set a secure cookie on successful login. This is something I plan to address in a separate post. For now, you’ll need to store the token on the client side and pass an Authorization header in all your graphql queries. This relay network layer makes that a breeze.

fetch(API_URL + '/login', {
      method: 'POST',
      headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
      body: JSON.stringify({
          "email": email,
          "password": password
      })})
      .then(response => response.ok ? response.json() : response.json().then(err => Promise.reject(err)))
      .then(json => login_callback(json.access_token))
      .catch(err => {
        return Promise.reject(err);
      });

To create a test user, you can use the python REPL.

$ python
>>> from models import User
>>> user = User(email="[email protected]", first_name="Me", last_name="Myself")
>>> user.password = "yourpass"
>>> user.save()

You can test your backend using something like Postman. Create a post request to http://127.0.0.1:5000/login of type raw->Json and add the body {“email”:”[email protected]”,”password”:”yourpass”}

Alternatively, you can also use curl from the command line

$ curl -X POST -H "Content-Type: application/json" -d '{"email":"[email protected]","password":"yourpass"}' "http://127.0.0.1:5000/login"
$ curl -X GET -H "Authorization: JWT [token from previous curl request]" "http://127.0.0.1:5000/graphql?query=\{viewer\{id,firstName,lastName,email\}\}"

For more on using graphql with python, refer to the docs of the excellent graphene library

To skip all the copy pasting, you can get this example on github