React, Python and GraphQL: server-side

September 12, 2018

This is the second post in a series dedicated to building modern web applications with React and Python, using a GraphQL API.

If you haven’t already, make sure you start reading from the first post.

In this post, we’re going to cover the design and implementation of the server-side service, exposing a GraphQL API to be consumed by our web client.

API design

Our API needs to expose functionality for:

  • Listing messages in a chat channel
  • Post a new message to a channel
  • Process authentication (exchange username / password with a token)

We’ll also need real-time notifications about new messages, but that’s going to be covered in a later post.

type Message {
    id: Int
    timestamp: DateTime
    text: String
    author: User
}

type User {
    name: String
}

type AuthToken {
    token: String  # JWT, treated opaquely by the client
}

type Query {
    messages(channel: String!): [Message]
}

type Mutation {
    postMessage(channel: String!, text: String!): PostMessageResult
    authenticate($email: String!, $password: String!): AuthToken
}

type Subscription {
    newMessages(channel: String!): [Message]
}

Tech stack

  • Web framework: Flask (with flask-cors, flask-sockets)
  • WSGI container: gunicorn
  • GraphQL executor: Graphene
  • Primary database: PostgreSQL (via SQLAlchemy Core)

Install the required dependencies

First, make sure you have pipenv and Python (3.6 or above is recommended).

# Base dependencies for the API
pipenv install flask graphene flask-graphql gunicorn flask-cors nicelog

# Database support
pipenv install sqlalchemy psycopg2 alembic

# For websockets support (covered in a later post)
pipenv install flask-sockets graphql-ws rx redis

General code organization

I put all the backend code in a single Python package, to keep things tidy. This is a brief explanation of the general layout

yawc                          # Base package
├── app.py                    # Definition of the Flask application
├── auth.py                   # Authentication functions
├── cli.py                    # Command-line administration tool
├── db                        # Database-related code
│   ├── connection.py         # Initialize database connection, transaction helpers
│   ├── query                 # Package containing functions that run database queries
│   └── schema                # Package containing db schema definitions
├── schema.py                 # GraphQL schema definition
├── utils                     # Package containing misc utilities
└── wsgi.py                   # Main entry point for production

All the database-related code is contained in the db package. Only functions defined in yawc.db.query sub-modules are allowed to directly run queries against the database.

This means all the rest of the codebase needs not be aware of the actual data model structure, and doesn’t (ideally) need changes in case the data model is changed in the future.

Data persistence

We need to store two types of objects:

  • users, containing authentication information
  • messages, containing the actual chat messages

Here’s the package containing schema definitions:

https://github.com/rshk/yawc/tree/master/yawc/db/schema

Database connection boilerplate

See: https://github.com/rshk/yawc/blob/master/yawc/db/connection.py

A connection() function provides convenient access to a database connection.

The transaction() context manager automatically handles transactions, including nested transaction support (especially useful to speed up testing).

Development database server

For convenience, we’ll use Docker and Docker Compose to run our development database server.

The following docker-compose.yml will do:

version: '2'
services:

  # WARNING: DO NOT name this just "postgres" or it will cause things
  # to break, for some reason.
  yawc-db:
    image: postgres:10
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ''
      POSTGRES_DB: 'yawc'
    volumes:
      - yawc-db-pgdata:/var/lib/postgresql/data
    ports:
      # HOST:CONTAINER
      - "127.0.0.1:5432:5432"

volumes:
  yawc-db-pgdata:

Now start it with docker-compose up.

Also, make sure you add database configuration to .env:

DATABASE_URL=postgres://postgres:@localhost:5432/yawc

Database migrations support (via Alembic)

We need to set up Alembic for creating and applying database migrations.

To initialize a new Alembic configuration folder:

pipenv run alembic init ./migrations

We need to make a few changes, most importantly set the target_metadata variable to point to our Metadata instance we’re registering tables to.

In migrations/env.py:

import os
from yawc import db

# Take DATABASE_URL from the environment
config.set_main_option('sqlalchemy.url', os.environ['DATABASE_URL'])

target_metadata = db.metadata

Create initial migration:

pipenv run alembic revision --autogenerate -m 'Initial'

You’ll see a file named something like migrations/versions/cd79e319cfd5_initial.py. Double-check that it contains all the correct migration code.

Finally, apply it to the database:

pipenv run alembic upgrade head

Query functions

Functions to actually make queries to the database are defined in the yawc.db.query package.

GraphQL schema definition

Our GraphQL schema is defined in yawc.schema.

It mostly reflects the GraphQL schema mentioned before, making use of database query functions to manipulate the underlying data.

Application definition

We can now start putting things together.

In yawc.app, a create_app() function is responsible for creating a new instance of a Flask app suitable for serving our GraphQL API.

app = Flask(__name__)

app.add_url_rule(
    '/graphql',
    view_func=load_auth_info(GraphQLView.as_view(
        'graphql', schema=schema, graphiql=True)))

# Optional, for adding batch query support (used in Apollo-Client)
app.add_url_rule(
    '/graphql/batch',
    view_func=load_auth_info(GraphQLView.as_view(
        'graphql-batch', schema=schema, batch=True)))

We also need to add CORS support:

from flask_cors import CORS

CORS(app)

Authentication

You might have noticed the load_auth_info() decorator being applied to the GraphQLView in the previous section.

It is defined in yawc.auth, and provides functionality for loading authentication information from an HTTP request, and attaching information about the current user to the GraphQL execution context.

In this application we’re using JSON web tokens, but a similar approach can work with tokens stored in the database as well.

Resolver functions will be able to access that information and use it for checking authorization, etc.

Testing out things

We now have all the components needed to make it run, so we can give it a go:

pipenv run python -m yawc run -p 5000

Now head to http://localhost:5000/graphql

If everything is working fine, you’ll be presented with the GraphiQL interface, from which you can test some queries.

In the left pane, type in this query:

{
  messages(channel: "hello") {
    edges {
      id
      timestamp
      channel
      user {
        name
      }
      text
    }
  }
}

then hit Ctrl+Enter to execute.

If everything goes well, you should see something like this in the right (results) pane:

{
  "data": {
    "messages": {
      "edges": [
        {
          "id": "1",
          "timestamp": "2018-07-04T18:22:50.576067",
          "channel": "hello",
          "user": {
            "name": "Hello"
          },
          "text": "It works!"
        }
      ]
    }
  }
}

In the next post, we’ll see how to set up and use a GraphQL client in our front-end code, to actually make use of the API we just created.


Samuele Santi

Written by Samuele Santi, who is using technology to make the world a better place. You can follow him on Twitter @_rshk