Getting Started#
This document will go over the basics of building a GraphQL server. We will cover:
- Defining your types and schema
- Adding custom resolvers to your schema
- Executing client queries against the schema
- Exposing the schema behind an HTTP endpoint (using Flask)
If you haven’t done so already, please take a moment to install py-gql before continuing.
Defining your schema#
The first step in creating a GraphQL server is defining the types and schema that clients will be able to query. The GraphQL schema that we’ll implement for this example is as follow (using the schema definition language):
enum CharacterType {
Human,
Droid,
}
type Query {
hero: Person!,
characters: [Person]!,
character(id: Int!): Person,
}
type Person {
id: Int!,
name: String,
type: CharacterType,
}
The easiest way to build an executable schema for our server is through
py_gql.build_schema()
which does so by consuming the SDL. This is often
referred to as a schema first approach to schema definition, as opposed to code
first where we’d build a schema from Python objects.
from py_gql import build_schema
SDL = """
...
"""
schema = build_schema(SDL)
Interacting with your data through resolver functions#
The schema as defined above isn’t going to be very useful unless we have a way to interact with our data.
For this example we will use the following JSON dataset:
[
{
"type": "Droid",
"id": 2000,
"name": "C-3PO"
},
{
"type": "Droid",
"id": 2001,
"name": "R2-D2"
},
{
"type": "Human",
"id": 1000,
"name": "Luke Skywalker"
},
{
"type": "Human",
"id": 1001,
"name": "Darth Vader"
},
{
"type": "Human",
"id": 1002,
"name": "Han Solo"
},
{
"type": "Human",
"id": 1003,
"name": "Leia Organa"
},
{
"type": "Human",
"id": 1004,
"name": "Wilhuff Tarkin"
}
]
Let’s implement our resolver functions:
@schema.resolver("Query.hero")
def resolve_hero(_root, ctx, _info):
return ctx["db"][2000] # R2-D2
@schema.resolver("Query.characters")
def resolve_characters(_root, ctx, _info):
return ctx["db"].items()
@schema.resolver("Query.character")
def resolve_character(_root, ctx, _info, *, id):
return ctx["db"].get(id, None)
While we won’t go into details concerning the resolvers signature we can already note that they receive the following parameters:
- The resolved parent value (The server provided root value for top-level fields)
- A context value which is where you provide any application specific data such
as database connections, loggers, etc. In this case the
db
part of the context will be a mapping for each entry’s id to itself. - An info object which carries some GraphQL specific data about the current execution context.
- The GraphQL field arguments are passed as keyword parameters.
We haven’t implemented any resolver for the field on the Character
type.
That is because the default behavior is to look up field of dictionaries and
objects which is sufficient here.
Refer to Defining resolvers for more details on implementing resolvers.
Executing queries against the schema#
Now that we have a valid schema, we just need to provide client queries for
execution. The execution is carried out by py_gql.graphql_blocking()
(as
all our resolvers are blocking, for different runtimes such as Python’s asyncio
we’d use py_gql.graphql()
) and consists of 3 steps:
- Parsing the client query and validating that it is a correct GraphQL document
- Validating the query against the schema to verify that it can be executed at all
- Execute the query and surface any errors or return the data
The return value is an instance of py_gql.GraphQLResult
which can be
serialized using py_gql.GraphQLResult.response()
.
We will expose this behind an HTTP API using Flask.
Note
While the transport and serialization format depend on the application and
are technically irrelevant to the GraphQL runtime itself; it is pretty
common to expose GraphQL APIs behind an HTTP server, traditionally under
POST /graphql
and JSON encode the request and response which is what
we’re doing here.
app = flask.Flask(__name__)
@app.route("/graphql", methods=("POST",))
def graphql_route():
data = flask.request.json
result = graphql_blocking(
schema,
data["query"],
variables=data.get("variables", {}),
operation_name=data.get("operation_name"),
context=dict(db=database),
)
return flask.jsonify(result.response())
Pulling together all that we’ve seen so far you should have a working GraphQL server that you can test using any HTTP client.
Using HTTPie for example (note that the response might
be reordered due to how HTTPie prints json, use --pretty=none
to see the
raw response):
$ http --json POST :5000/graphql query='{ hero { id name type } }'
HTTP/1.0 200 OK
...
{
"data": {
"hero": {
"id": 2000,
"name": "C-3PO",
"type": "Droid"
}
}
}
$ http --json POST :5000/graphql query='{ character(id: 10) { name } }'
HTTP/1.0 200 OK
...
{
"data": {
"character": null
}
}
$ http --json POST :5000/graphql query='{ character(id: 1000) { name } }'
HTTP/1.0 200 OK
...
{
"data": {
"character": {
"name": "Luke Skywalker"
}
}
}
$ http --json POST :5000/graphql query='{ character(id: 1000) }'
HTTP/1.0 200 OK
...
{
"errors": [
{
"locations": [
{
"column": 3,
"line": 1
}
],
"message": "Field \"character\" of type \"Person\" must have a subselection"
}
]
}
Complete code#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | # -*- coding: utf-8 -*-
"""
Minimal getting started example with Flask.
To run this, make sure Flask and py_gql are installed and run:
FLASK_APP=getting-started.py flask run
"""
import json
import flask
from py_gql import build_schema, graphql_blocking
SDL = """
enum CharacterType {
Human,
Droid,
}
type Query {
hero: Person!,
characters: [Person]!,
character(id: Int!): Person,
}
type Person {
id: Int!,
name: String,
type: CharacterType,
}
"""
DATA = """
[
{
"type": "Droid",
"id": 2000,
"name": "C-3PO"
},
{
"type": "Droid",
"id": 2001,
"name": "R2-D2"
},
{
"type": "Human",
"id": 1000,
"name": "Luke Skywalker"
},
{
"type": "Human",
"id": 1001,
"name": "Darth Vader"
},
{
"type": "Human",
"id": 1002,
"name": "Han Solo"
},
{
"type": "Human",
"id": 1003,
"name": "Leia Organa"
},
{
"type": "Human",
"id": 1004,
"name": "Wilhuff Tarkin"
}
]
"""
schema = build_schema(SDL)
database = {row["id"]: row for row in json.loads(DATA)}
@schema.resolver("Query.hero")
def resolve_hero(_root, ctx, _info):
return ctx["db"][2000] # R2-D2
@schema.resolver("Query.characters")
def resolve_characters(_root, ctx, _info):
return ctx["db"].items()
@schema.resolver("Query.character")
def resolve_character(_root, ctx, _info, *, id):
return ctx["db"].get(id, None)
app = flask.Flask(__name__)
@app.route("/graphql", methods=("POST",))
def graphql_route():
data = flask.request.json
result = graphql_blocking(
schema,
data["query"],
variables=data.get("variables", {}),
operation_name=data.get("operation_name"),
context=dict(db=database),
)
return flask.jsonify(result.response())
|