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.
Our API needs to expose functionality for:
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]
}
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
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.
We need to store two types of objects:
Here’s the package containing schema definitions:
https://github.com/rshk/yawc/tree/master/yawc/db/schema
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).
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
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
Functions to actually make queries to the database are defined in the yawc.db.query package.
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.
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)
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.
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.
Written by Samuele Santi, who is using technology to make the world a better place. You can follow him on Twitter @_rshk