Introduction to GraphQL
Johnson Liang
Frontend Engineer, Appier
2017.08.09
This work by Appier Inc is licensed under a Creative Commons Attribution 4.0 International License.
Agenda
graphql.org
[Server]
API shape;
GraphQL Schema
[Client]
Query;
GraphQL
[Client]
Result
Fundamental parts
of a GraphQL server
Runnable GraphQL server code
const {
graphql, GraphQLSchema, GraphQLObjectType,� GraphQLString,�} = require('graphql');��const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� serverTime: {� type: GraphQLString,� resolve() {� return (new Date).toLocaleString();� },� },� },� })�});��const query = `query { serverTime }`;��graphql(schema, query).then(result =>{
console.log(result.data);
});��// Prints:�// {serverTime: "9/5/2016, 6:28:46 PM"}
import graphene�from datetime import datetime�
�class Query(graphene.ObjectType):� server_time = graphene.Field(graphene.String)�� def resolve_server_time(obj, args, context, info):� return str(datetime.now())��schema = graphene.Schema(query=Query)
�query = 'query { serverTime }'��result = schema.execute(query)�print(result.data)
�#: Prints: OrderedDict([('serverTime', '2017-07-24 19:10:38.127089')])
Runnable GraphQL server code / Defining API shape (GraphQL schema)
const {
graphql, GraphQLSchema, GraphQLObjectType,� GraphQLString,�} = require('graphql');��const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� serverTime: {� type: GraphQLString,� resolve() {� return (new Date).toLocaleString();� },� },� },� })�});��const query = `query { serverTime }`;��graphql(schema, query).then(result =>{
console.log(result.data);
});��// Prints:�// {serverTime: "9/5/2016, 6:28:46 PM"}
import graphene�from datetime import datetime�
�class Query(graphene.ObjectType):� server_time = graphene.Field(graphene.String)�� def resolve_server_time(obj, args, context, info):� return str(datetime.now())��schema = graphene.Schema(query=Query)
�query = 'query { serverTime }'��result = schema.execute(query)�print(result.data)
�#: Prints: OrderedDict([('serverTime', '2017-07-24 19:10:38.127089')])
Runnable GraphQL server code / Query in GraphQL
const {
graphql, GraphQLSchema, GraphQLObjectType,� GraphQLString,�} = require('graphql');��const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� serverTime: {� type: GraphQLString,� resolve() {� return (new Date).toLocaleString();� },� },� },� })�});��const query = `query { serverTime }`;��graphql(schema, query).then(result =>{
console.log(result.data);
});��// Prints:�// {serverTime: "9/5/2016, 6:28:46 PM"}
import graphene�from datetime import datetime�
�class Query(graphene.ObjectType):� server_time = graphene.Field(graphene.String)�� def resolve_server_time(obj, args, context, info):� return str(datetime.now())��schema = graphene.Schema(query=Query)
�query = 'query { serverTime }'��result = schema.execute(query)�print(result.data)
�#: Prints: OrderedDict([('serverTime', '2017-07-24 19:10:38.127089')])
Runnable GraphQL server code / Query Execution (query + schema → result)
const {
graphql, GraphQLSchema, GraphQLObjectType,� GraphQLString,�} = require('graphql');��const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� serverTime: {� type: GraphQLString,� resolve() {� return (new Date).toLocaleString();� },� },� },� })�});��const query = `query { serverTime }`;��graphql(schema, query).then(result =>{
console.log(result.data);
});��// Prints:�// {serverTime: "9/5/2016, 6:28:46 PM"}
import graphene�from datetime import datetime�
�class Query(graphene.ObjectType):� server_time = graphene.Field(graphene.String)�� def resolve_server_time(obj, args, context, info):� return str(datetime.now())��schema = graphene.Schema(query=Query)
�query = 'query { serverTime }'��result = schema.execute(query)�print(result.data)
�#: Prints: OrderedDict([('serverTime', '2017-07-24 19:10:38.127089')])
Runnable GraphQL server code / Result
const {
graphql, GraphQLSchema, GraphQLObjectType,� GraphQLString,�} = require('graphql');��const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� serverTime: {� type: GraphQLString,� resolve() {� return (new Date).toLocaleString();� },� },� },� })�});��const query = `query { serverTime }`;��graphql(schema, query).then(result =>{
console.log(result.data);
});��// Prints:�// {serverTime: "9/5/2016, 6:28:46 PM"}
import graphene�from datetime import datetime�
�class Query(graphene.ObjectType):� server_time = graphene.Field(graphene.String)�� def resolve_server_time(obj, args, context, info):� return str(datetime.now())��schema = graphene.Schema(query=Query)
�query = 'query { serverTime }'��result = schema.execute(query)�print(result.data)
�#: Prints: OrderedDict([('serverTime', '2017-07-24 19:10:38.127089')])
Parts in GraphQL server
GraphQL Server
query { serverTime }
Query
{ data: {
serverTime: "..."
} }
Result
Schema
GraphQL server over HTTP
const {
graphql, GraphQLSchema, GraphQLObjectType,� GraphQLString,�} = require('graphql');��const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� serverTime: {� type: GraphQLString,� resolve() {� return (new Date).toLocaleString();� },� },� },� })�});��const query = `query { serverTime }`;��graphql(schema, query).then(result =>{
console.log(result.data);
});��
import graphene�from datetime import datetime�
�class Query(graphene.ObjectType):� server_time = graphene.Field(graphene.String)�� def resolve_server_time(obj, args, context, info):� return str(datetime.now())��schema = graphene.Schema(query=Query)
�query = 'query { serverTime }'��result = schema.execute(query)�print(result.data)
�
GraphQL server over HTTP
const {
graphql, GraphQLSchema, GraphQLObjectType,� GraphQLString,�} = require('graphql');��const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� serverTime: {� type: GraphQLString,� resolve() {� return (new Date).toLocaleString();� },� },� },� })�});��app.post('/graphql', (req, res) => {� graphql(schema, req.body).then(res.json);�});��
import graphene�from datetime import datetime�
�class Query(graphene.ObjectType):� server_time = graphene.Field(graphene.String)�� def resolve_server_time(obj, args, context, info):� return str(datetime.now())��schema = graphene.Schema(query=Query)
�@app.route('/graphql')�def graphql():� result = schema.execute(request.body)� return json.dumps({� data: result.data,� errors: result.errors� })
�
Ready-made GraphQL Server library
NodeJS
Python
Running Example
GraphiQL
GraphQL Results & error handling
{
serverTime
}
{� "data": {� "serverTime":� "9/5/2016, 6:28:46 PM"� }�}
{� "data": {� "serverTime": null,� },� "errors": {� "message": "...",� "locations": [...]� }�}
{� "errors": [� {� "message": "...",� "locations": [...]� }� ]�}
Query
Result
Runtime Error
Parse Error
Defining API shape -
GraphQL schema
Query & Schema
const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� serverTime: {� type: GraphQLString,� resolve() { /* ...*/ },� },� },� })�});
class Query(graphene.ObjectType):� server_time = graphene.Field(...)�� def resolve_server_time(obj, args, context, info):� # ...��schema = graphene.Schema(query=Query)
query {
serverTime
}
Object Type
const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� serverTime: {� type: GraphQLString,� resolve() { /* ...*/ },� },� },� })�});
class Query(graphene.ObjectType):� server_time = graphene.Field(...)�� def resolve_server_time(obj, args, context, info):� # ...��schema = graphene.Schema(query=Query)
query {
serverTime
}
Object Type / Fields
class Query(graphene.ObjectType):� server_time = graphene.Field(...)�� def resolve_server_time(obj, args, context, info):� # ...��schema = graphene.Schema(query=Query)
const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� serverTime: {� type: GraphQLString,� resolve() { /* ...*/ },� },� },� })�});
query {
serverTime
}
Object Type / Fields / Resolvers
const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� serverTime: {� type: GraphQLString,� resolve() { /* ...*/ },� },� },� })�});
class Query(graphene.ObjectType):� server_time = graphene.Field(...)�� def resolve_server_time(obj, args, context, info):� # ...��schema = graphene.Schema(query=Query)
query {
serverTime
}
Object Type / Fields / Types
Object Type / Fields / Types (2)
Field with a Object Type
const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� serverTime: {� type: Time,� resolve() { /* ...*/ },� },� },� })�});
class Query(graphene.ObjectType):� server_time = graphene.Field(Time)�� def resolve_server_time(obj, args, context, info):� # ...��schema = graphene.Schema(query=Query)
query {
serverTime { hour, minute, second }
}
Field with a Object Type
class Time(graphene.ObjeceType):� hour = graphene.Int� minute = graphene.Int� second = graphene.Int�
const Time = new GraphQLObjectType({� name: 'Time',� fields: {� hour: {type: GraphQLInt},� minute: {type: GraphQLInt},� second: {type: GraphQLInt},� }�});
query {
serverTime { hour, minute, second }
}
Schema, query and output
Real-world example (simplified from Cofacts API server)
query {� article(...) {...}� reply {� author {...}� }�}
const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� article: {� type: ArticleType,� args: {...},� resolve() {...}� },� articles: {� type: new GraphQLList(ArticleType),� resolve() {...}� },� reply: {� type: new GraphQLObjectType({� name: 'ReplyType',� fields: {� author: {� type: userType,� resolve() {...}� },� ...� }� }),� resolve() {...}�
{� data: {� article: {...},� reply: {� author: {...}� }� }�}
schema
query input
output
� query��� article��������� reply���� author
Resolving object fields
Resolving a field
const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� serverTime: {� type: Time,� resolve() { /* ...*/ },� },� },� })�});
class Query(graphene.ObjectType):� serverTime = graphene.Field(Time)�� def resolve_server_time(obj, args, context, info):� # ...��schema = graphene.Schema(query=Query)
Resolver function signature
Resolver function signature / obj
(Text from official documentation)
Resolver function signature / obj
class Time(graphene.ObjectType):� hour = graphene.Int()� minute = graphene.Int()� second = graphene.Int()�� def resolve_hour(obj, args, context, info):� return obj.hour�� def resolve_minute(obj, args, context, info):� return obj.minute�� def resolve_second(obj, args, context, info):� return obj.second
�class Query(graphene.ObjectType):� server_time = graphene.Field(Time)�� def resolve_server_time(obj, args, context, info):� return datetime.now()��schema = graphene.Schema(query=Query)
const Time = new GraphQLObjectType({� name: "Time",� fields: {� hour: {
type: GraphQLInt,� resolve: obj => obj.getHours() },� minute: {
type: GraphQLInt,� resolve: obj => obj.getMinutes() },� second: {
type: GraphQLInt,� resolve: obj => obj.getSeconds() }, }�});��const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: "Query",� fields: {� serverTime: {� type: Time,� resolve(){ return new Date; }� }� }� })�});
How resolver functions are invoked
Trivial resolvers
someField(obj) {� return obj.someField;�}
class Time(graphene.ObjectType):� hour = graphene.Int()� minute = graphene.Int()� second = graphene.Int()��
�class Query(graphene.ObjectType):� server_time = graphene.Field(Time)�� def resolve_server_time(obj, args, context, info):� return datetime.now()��schema = graphene.Schema(query=Query)
const Time = new GraphQLObjectType({� name: "Time",� fields: {� hour: {type: GraphQLInt},� minute: {type: GraphQLInt},� second: {type: GraphQLInt}});��const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: "Query",� fields: {� serverTime: {� type: Time,� resolve(){� const date = new Date;� return {� hour: date.getHours(),� minute: date.getMinutes(),� second: date.getSeconds(),
};� }� }� }� })�});
resolve_some_field(obj):� return obj.some_field
has property hour, minute, second
Root value
const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: "Query",� fields: {� serverTime: {� type: Time,� resolve(obj){ ... }� }� }� })�});
graphql(schema, query, rootValue)
class Query(graphene.ObjectType):� server_time = graphene.Field(Time)�� def resolve_server_time(obj):� return ...�
�schema = graphene.Schema(query=Query)
schema.execute(
query,
root_value=root_value
)
Resolver function signature / args
�(Text from official documentation)
Arguments when querying a field
const schema = new GraphQLSchema({� query: new GraphQLObjectType({� name: 'Query',� fields: {� serverTime: {� type: GraphQLString,� args: {� timezone: {� type: GraphQLString,� description: 'Timezone name (Area/City)',� },� },� resolve(obj, { timezone = 'Asia/Taipei' }) {� return (new Date()).toLocaleString({� timeZone: timezone,� });;� },� },� },� }),�});
class Query(graphene.ObjectType):� server_time = graphene.Field(� graphene.String,� timezone=graphene.Argument(� graphene.Int,� default_value=8,� description="UTC+N, N=-24~24."� )� )�
� def resolve_server_time(obj, args, context, info):� tz = timezone(timedelta(hours=args['timezone']))� return str(datetime.now(tz))
�schema = graphene.Schema(query=Query)
query {
serverTime(timezone: "UTC") {hour}
}
query {
serverTime(timezone: 0) {hour}
}
Args and Input Object Type
query {
serverTime ( timezone: "UTC", offset: { hour: 3, minutes: 30 } ) {
hour
}
}
Resolver function signature / context
(Text from official documentation)
Context
#: Time object's hour resolver�def resolve_hour(obj, args, context, info):� return ...�
�#: Root Qyery type's serverTime resolver�def resolve_server_time(� obj, args, context, info�):� return ...��#: Execute the query�schema = graphene.Schema(
query=Query, context=context)
// Time object type's field�hour: {� type: GraphQLInt,� resolve(obj, args, context) {...}�}��// Root Query type's field�serverTime: {� type: Time,� resolve(obj, args, context) {...},�}��// Executing query�graphql(� schema, query, rootValue, context�)
Mutative APIs
query {
createUser(name:"John Doe"){
id
}
}
query {
user(name:"John Doe"){ id }
article(id:123) { text }
}
mutation {
createUser(...) {...}
createArticle(...) {...}
}
Run in parallel
Run in series
Mutations
class CreatePerson(graphene.Mutation):� class Input:� name = graphene.Argument(graphene.String)�� ok = graphene.Field(graphene.Boolean)� person = graphene.Field(lambda: Person)�� @staticmethod� def mutate(root, args, context, info):� #: Should do something that persists� #: the new Person here� person = Person(name=args.get('name'))� ok = True� return CreatePerson(person=person, ok=ok)��class Mutation(graphene.ObjectType):� create_person = CreatePerson.Field()��schema = graphene.Schema(� query=..., mutation=Mutation�)
const CreatePersonResult = new GraphQLObjectType({� name: 'CreatePersonResult', fields: {� ok: { type: GraphQLBoolean },� person: { type: Person },� }�})��const schema = new GraphQLSchema({� query: ...,� mutation: new GraphQLObjectType({� name: 'Mutation',� fields: {� CreatePerson: {� type: CreatePersonResult,� args: { name: { type: GraphQLString } },� resolve(obj, args) {� const person = {name: args.name};� // Should do something that persists� // the person� return { ok: true, person };� }� }� }� })�})
Mutations
class CreatePerson(graphene.Mutation):� class Input:� name = graphene.Argument(graphene.String)�� ok = graphene.Field(graphene.Boolean)� person = graphene.Field(lambda: Person)�� @staticmethod� def mutate(root, args, context, info):� #: Should do something that persists� #: the new Person here� person = Person(name=args.get('name'))� ok = True� return CreatePerson(person=person, ok=ok)��class Mutation(graphene.ObjectType):� create_person = CreatePerson.Field()��schema = graphene.Schema(� query=..., mutation=Mutation�)
const CreatePersonResult = new GraphQLObjectType({� name: 'CreatePersonResult', fields: {� ok: { type: GraphQLBoolean },� person: { type: Person },� }�})��const schema = new GraphQLSchema({� query: ...,� mutation: new GraphQLObjectType({� name: 'Mutation',� fields: {� CreatePerson: {� type: CreatePersonResult,� args: { name: { type: GraphQLString } },� resolve(obj, args) {� const person = {name: args.name};� // Should do something that persists� // the person� return { ok: true, person };� }� }� }� })�})
Mutations
class CreatePerson(graphene.Mutation):� class Input:� name = graphene.Argument(graphene.String)�� ok = graphene.Field(graphene.Boolean)� person = graphene.Field(lambda: Person)�� @staticmethod� def mutate(root, args, context, info):� #: Should do something that persists� #: the new Person here� person = Person(name=args.get('name'))� ok = True� return CreatePerson(person=person, ok=ok)��class Mutation(graphene.ObjectType):� create_person = CreatePerson.Field()��schema = graphene.Schema(� query=..., mutation=Mutation�)
const CreatePersonResult = new GraphQLObjectType({� name: 'CreatePersonResult', fields: {� ok: { type: GraphQLBoolean },� person: { type: Person },� }�})��const schema = new GraphQLSchema({� query: ...,� mutation: new GraphQLObjectType({� name: 'Mutation',� fields: {� CreatePerson: {� type: CreatePersonResult,� args: { name: { type: GraphQLString } },� resolve(obj, args) {� const person = {name: args.name};� // Should do something that persists� // the person� return { ok: true, person };� }� }� }� })�})
Extended reading -- Designing GraphQL Mutations
Making requests to GraphQL Servers
Talk to GraphQL server via HTTP
POST /graphql HTTP/1.1
Content-Type: application/json
{
"query": "...GraphQL Query string...",
}
Working with GraphQL Variables (Text from official documentation)
1
2
3
Working with GraphQL Variables (Text from official documentation)
POST /graphql HTTP/1.1
Content-Type: application/json
{� "query":� "query($offset:TimeInput){serverTimeWithInput(offset:$offset)}",� "variables": {� "offset": { "hour": 3 }� }�}
Some old GraphQL servers only accept "variables" as JSON strings
1
2
3
Clients
Advanced query techniques
Extended reading: The Anatomy of GraphQL queries
Solving N+1 Problem:
Dataloader
How resolver functions are invoked
N+1 problem
{
users {
bestFriend {
displayName
}
}
}
Tackling N+1 problem (1)
{
users {
bestFriend {
displayName
}
}
}
Solution 1: Resolve bestFriend in users' resolver
Tackling N+1 problem (2)
{
users {
bestFriend {
displayName
}
}
}
Solution 2: Query all bestFriend in a batch
Dataloader - Batching & caching utility
import DataLoader from 'dataloader';�
const userLoader = new DataLoader(� ids => {
console.log('Invoked with', ids);
return User.getAllByIds(ids);
}�);
�userLoader.load(1).then(console.log)�userLoader.load(2).then(console.log)�userLoader.load(3).then(console.log)
userLoader.load(1).then(console.log)
�// Outputs:�// Invoked with [1,2,3]�// {id: 1, ...}�// {id: 2, ...}�// {id: 3, ...}
// {id: 1, ...}
from aiodataloader import DataLoader��class UserLoader(DataLoader):� async def batch_load_fn(self, ids):� print('Invoked with %s' % ids)� return User.get_all_by_ids(ids)�
user_loader = UserLoader()
future1 = user_loader.load(1)�future2 = user_loader.load(2)�future3 = user_loader.load(3)�future4 = user_loader.load(1) # == future1�
# prints:
# Invoked with [1, 2, 3]�print(await future1) # {id: 1, ...}�print(await future2) # {id: 2, ...}�print(await future3) # {id: 3, ...}�print(await future4) # {id: 1, ...}
�
Define a batch function
Call load() whenever you want to
Get data you need
Batch function
class UserLoader(DataLoader):� async def batch_load_fn(self, ids):� """Fake data loading"""� return [{'id': id} for id in ids]
��
const userLoader = new DataLoader(ids => {� // Fake data loading� return Promise.resolve(ids.map(id => ({id})))�})
dataloader instance methods
Multiple dataloader instances
user_loader = UserLoader()�articles_by_author_id_loader = ArticlesByAuthorIdLoader()��# Get user 1's best friends's articles�user = await user_loader.load(1)�print(await articles_by_author_id_loader.load(� user.best_friend_id�))�
�# prints:�# [article1, article2, ...]�
const userLoader = new DataLoader(...)�const articlesByAuthorIdLoader = new DataLoader(...)��// Get user 1's best friends's articles�userLoader.load(1).then(� ({bestFriendId}) =>� articlesByAuthorIdLoader.load(bestFriendId)�).then(console.log)
�// prints:�// [article1, article2, ...]
Combine dataloader with GraphQL schema
# in root query type�user = graphene.Field(User)�def resolve_user(obj, args, context):� return context['user_loader'].load(args['id'])�
�# in user object type�best_friend_articles =� graphene.Field(graphene.List(Article))�def resolve_best_friend_articles(obj, args, context):� return context['articles_by_author_id_loader'].load(� obj['best_friend_id']� )
app.post('/graphql', bodyParser.json(), � graphqlExpress(req => ({� schema,� context: {� userLoader: new DataLoader(...),� articleByUserIdLoader: new DataLoader(...)� }� }))�)
// field "user" in root query type�{ type: User,� resolve(obj, {id}, {userLoader}) {� return userLoader.load(id);� } }��// field "bestFriendArticles" in User object type�{ type: new GraphQLList(Article),� resolve({bestFriendId}, args,� {articleByUserIdLoader}
) {� return articleByUserIdLoader.load(bestFriendId);� } }
class ViewWithContext(GraphQLView):� def get_context(self, request):� return {� 'user_loader': UserLoader(),� 'articles_by_author_id_loader':� ArticlesByAuthorIdLoader()� }�app.add_url_rule(� '/graphql', view_func=ViewWithContext.as_view(� 'graphql', schema=schema, graphiql=True,� executor=AsyncioExecutor) )
Summary
This work by Appier Inc is licensed under a Creative Commons Attribution 4.0 International License.
Other Resources
This work by Appier Inc is licensed under a Creative Commons Attribution 4.0 International License.