Overview of GraphQL & Clients

Preview:

Citation preview

Overview ofGraphQL & Clients

! # @zetavgfb.me/pokaichang72

My Background⬢ Building stuff as a web developer from

2012 ⬢ Shallow experiences covered from

design, mobile, front-end and backend develop to cloud deployments (AWS)

⬢ Fan of GraphQL/Relay of its beauty of API design since 2015

⬢ Working at with Ruby, JavaScript (React.js) and playing Elixir

⬢ Former tech lead at Colorgy

! # @zetavgfb.me/pokaichang72

⬡ Complain about RESTful Introduce GraphQL

⬡ Just enough GraphQL to get started

⬡ GraphQL client library overview

⬡ Intro to Relay

⬡ Demo: GraphQL & Relay on Railshttps://github.com/zetavg/RailsRelayTodoMVC

Outline

Background of API Developing

The evolution of API⬢ RESTful: Easy to use, easy to develop

⬡ Directly based on Wide World Web

⬡ URI as resource name (noun), HTTP method as action (verb)

⬡ We need documents: Swagger...

⬡ ...and type definitions: JSON Schema

⬡ ...and data relations: JSON API

⬡ Combine them all: API Blueprint, RAML

The evolution of API⬢ But for the front-end, especially SPA or mobile apps:

⬡ Querying complex data efficiently is still hard

⬡ We may come up with lots of endpoint versions

⬡ Or messy features on different endpoints

⬡ On purpose specs are hard to follow, without an clear interface, APIs tends to be hard to reuse and maintain

⬡ Writing code to fetch and store data is annoying

⬡ Caching is hard cause there's no explicit schema

⬡ Co-working may be messy cause there's no schema

/api/v1/posts.json

/api/v2/posts.json

/api/v3/posts.json

/api/v4/posts.json

/api/v65535/posts.json

⋯⋯

/api/posts.json

/api/posts.json?include=author

/api/posts.json?include=author,comments

/api/posts.json?cover=true&include=author,comments

/api/posts.json?cover=true&include=author,comments_with_author

API should be like this

Not this

GraphQL⬢ A new query language

⬢ Brief History:

⬡ 2012 - Used for Facebook mobile app

⬡ 2015 - Publicly released

⬡ 2017 - Now: GraphQL & Relay re-licensed under     MIT

⬢ Normally uses a single endpoint URL ( POST /graphql )

A Glance on GraphQL

All your application data can be represented as a graph

$

{ "name": "Pokai Chang", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌] }

$

{ "name": "Lucy", "bio":"...", "followers": [◌], "repos": [◌, ◌] }

$

{ "name": "Jasper", "bio":"...", "followers": [◌], "repos": [◌] }

$

{ "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌] }

!{ "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌] }

!{ "name": "Hello World", "description": "...", "stargazers": [◌, ◌] }

!{ "name": "Handy Util", "description": "...", "stargazers": [◌] }

!{ "name": "Awesome App", "description": "...", "stargazers": [◌] }

!{ "name": "Todo", "description": "...", "stargazers": [◌] }

!

{ }{ "viewer": ◌ }

A subset of the graph is used to show an UI

$

{ "name": "Jasper", "bio":"...", "followers": [◌], "repos": [◌] }

$

{ "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌] }

$

{ "name": "Lucy", "bio":"...", "followers": [◌], "repos": [◌, ◌] }

!{ "name": "Awesome App", "description": "...", "stargazers": [◌] }

!{ "name": "Todo", "description": "...", "stargazers": [◌] }

!{ "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌] }

!{ "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌] }

!{ "name": "Handy Util", "description": "...", "stargazers": [◌] }

{ }{ "viewer": ◌ }

$

{ "name": "Pokai Chang", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌] }

$ dddddddd

!{ "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌] }

!{ "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌] }

!{ "name": "Handy Util", "description": "...", "stargazers": [◌] }

!{ "name": "Awesome App", "description": "...", "stargazers": [◌] }

!{ "name": "Todo", "description": "...", "stargazers": [◌] }

{ }{ "viewer": ◌ }

$

{ "name": "Pokai Chang", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌] }

$

{ "name": "Lucy", "bio":"...", "followers": [◌], "repos": [◌, ◌] }

$

{ "name": "Jasper", "bio":"...", "followers": [◌], "repos": [◌] }

$

{ "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌] }

$$

$

$

$

$

!{ "name": "Handy Util", "description": "...", "stargazers": [◌] }

!{ "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌] }

$

{ "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌] }

!{ "name": "Awesome App", "description": "...", "stargazers": [◌] }

!{ "name": "Todo", "description": "...", "stargazers": [◌] }

!{ "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌] }

{ }{ "viewer": ◌ }

$ $ $

$ $ $

$ $ $

$ $

/

$

{ "name": "Lucy", "bio":"...", "followers": [◌], "repos": [◌, ◌] }

$

{ "name": "Jasper", "bio":"...", "followers": [◌], "repos": [◌] }

$

{ "name": "Pokai Chang", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌] }

$

{ "name": "Pokai Chang", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌] }

$

{ "name": "Lucy", "bio":"...", "followers": [◌], "repos": [◌, ◌] }

$

{ "name": "Jasper", "bio":"...", "followers": [◌], "repos": [◌] }

$

{ "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌] }

!{ "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌] }

!{ "name": "Hello World", "description": "...", "stargazers": [◌, ◌] }

!{ "name": "Handy Util", "description": "...", "stargazers": [◌] }

!{ "name": "Awesome App", "description": "...", "stargazers": [◌] }

!{ "name": "Todo", "description": "...", "stargazers": [◌] }

!

{ }{ "viewer": ◌ }

GraphQL

⬡ Complain about RESTful Introduce GraphQL

⬡ Just enough GraphQL to get started

⬡ GraphQL client library overview

⬡ Intro to Relay

⬡ Demo: GraphQL & Relay on Railshttps://github.com/zetavg/RailsRelayTodoMVC

Outline

Basic Query⬢ Starts with selecting fields on the query root ⬢ WYSIWYG

{ "data": { "viewer": { "name": "Pokai Chang" } } }

query { viewer { name } }

Basic Query⬢ Querying nested fields

{ "data": { "viewer": { "name": "Pokai Chang", "birthday": { "month": 7, "day": 2 } } } }

query { viewer { name birthday { month day } } }

Types⬢ Get the type of a object using the __typename

meta field

{ "data": { "viewer": { "__typename": "User", "birthday": { "__typename": "Date" } } } }

query { viewer { __typename birthday { __typename } } }

Type defs as docs# GraphQL query language query { viewer { name birthday { month day } following { name } } }

# GraphQL schema language type Query { viewer: User }

type User { name: String! birthday: Date followers: [User] following: [User] }

type Date { year: Integer month: Integer day: Integer }

Non-Null & Lists# GraphQL schema language type Query { viewer: User }

type User { name: String! birthday: Date followers: [User] following: [User] }

type Date { year: Integer month: Integer day: Integer }

[<thing>] means an array of <thing> objects

! means that the field is non-nullable

Arguments⬢ Arguments can be defined on fields

query { user(id: 1) { name } }

Arguments⬢ Nested fields also can have arguments

query { user(id: 1) { name repo(name: "awesome-graphql") { name description } } }

Variables⬢ A way to dynamically change arguments for fields

query ($userId: Int!, $repoName:String!) { user(id: $userId) { name repo(name: $repoName) { name description } } }

{ "userId": 1, "repoName": "awesome-graphql" }

+

Fragment

fragment profileFields on User { name bio avatarUrl }

query { viewer { ...profileFields }

user(id: 1) { ...profileFields } }

Pre-define a set of fieldson a type or interface

as meaningful fragment

Interfaces⬢ An abstract type that includes a set of fields that a

type must define to implement

⬢ Can be used for fragments

interface Actor { id: ID! name: String! avatarUrl: String! }

type User implements Actor { id: ID! name: String! avatarUrl: String! ... }

type Bot implements Actor { id: ID! name: String!

# Sample Query

fragment actorFields on Actor { name bio avatarUrl }

query { feed { actor { ...actorFields } verb object {

Mutate Data w/ Mutations⬢ Mutation queries lives under mutation instead of

query , and are ways how we can change the data ⬢ We can put the input data in arguments, changed

nodes will be returned in the selectable payload ⬢ It’s a convention like RESTful GET/POST that clients

rely on

mutation { addComment(input: { subjectId: 1, body: "Hi." }) { subject { comments { body } } } }

Input Types⬢ Yes, inputs are also typed

input AddCommentInput { subjectId: ID! body: String! }

mutation { addComment(input: { subjectId: 1, body: "Hi." }) { subject { comments { body } } } }

GraphiQL⬢ An open source GraphQL playground

Query tree⬢ Each query is a tree extracted from the graph

⬢ The query is resolved by traversing the tree and resolving each field

query { viewer { name bio repos { name description } } }

$

{ "name": "Jasper", "bio":"...", "followers": [◌], "repos": [◌] }

$

{ "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌]

$

{ "name": "Lucy", "bio":"...", "followers": [◌], "repos": [◌, ◌] }

!{ "name": "Awesome App", "description": "...", "stargazers": [◌] }

!{ "name": "Todo", "description": "...", "stargazers": [◌] }

!

!{ "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌] }

!{ "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌] }

!{ "name": "Handy Util", "description": "...", "stargazers": [◌] }

{ }{ "viewer": ◌ }

$

{ "name": "Pokai Chang", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌] }

Query tree

Query tree

!{ "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌] }

!{ "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌] }

!{ "name": "Handy Util", "description": "...", "stargazers": [◌] }

{ }{ "viewer": ◌ }

${ "name": "Pokai Chang", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌] }

graphql.org

⬡ Complain about RESTful Introduce GraphQL

⬡ Just enough GraphQL to get started

⬡ GraphQL client library overview

⬡ Intro to Relay

⬡ Demo: GraphQL & Relay on Railshttps://github.com/zetavg/RailsRelayTodoMVC

Outline

Fetching Pagination

Caching Update

Optimistic Update Realtime UI

Fetching

const View = (data) => UI

Redux data flow

View

State

subscribe

Redux Store

Redux data flow

View

State

ReducerAction

subscribe

prevState

Redux data flow

View

State

ReducerAction

subscribe

prevState

Backend ?

Redux data flow

View

State

ReducerAction

subscribe

prevState

Backend

Action

Action

Action

$

{ "name": "Neson", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌] }

$

{ "name": "Lucy", "bio":"...", "followers": [◌], "repos": [◌, ◌] }

$

{ "name": "Jasper", "bio":"...", "followers": [◌], "repos": [◌] }

$

{ "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌] }

!{ "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌] }

!{ "name": "Hello World", "description": "...", "stargazers": [◌, ◌] }

!{ "name": "Handy Util", "description": "...", "stargazers": [◌] }

!{ "name": "Awesome App", "description": "...", "stargazers": [◌] }

!{ "name": "Todo", "description": "...", "stargazers": [◌] }

!

{ }{ "viewer": ◌ }

GraphQL

Relay data fetching

View

$$

$ $

$

dd

$ $ $ $

dddddd

Relay Store

Relay data fetching

viewer { name bio }

Viewdddddddd

viewer { repos { name description } }

$

Relay Store

Relay data fetching

Viewdddddddd

viewer { repos { name description } }

Backend

query { viewer { name bio repos { name description } } }

$

Relay Store

viewer { name bio }

Relay data fetching

Viewdddddddd

viewer { repos { name description } }

Backend{ "data": { "viewer": { "name": "…", "bio": "…", "followers": […], "repos": […] } } }

$

viewer { name bio }

query { viewer { name bio repos { name description } } }

Relay data fetching

View

$ㄎㄎㄎㄎ

viewer { repos { name description } }

dddddddd

viewer { name bio }

View

Relay data fetching

viewer { name bio }

View

$ㄎㄎㄎㄎ

viewer { following { name } }

dddddddd

$

$

$

$

$

View

Relay data fetching

viewer { name bio }

View

$ㄎㄎㄎㄎ

viewer { following { name } }

dddddddd

$

$

$

$

$

Backend

query { viewer { following { name } } }

{ "data": { "viewer": { "following": […] } } }

View

Relay data fetching

viewer { name bio }

View

$ㄎㄎㄎㄎ

viewer { following { name } }

dddddddd

$

$

$

$

$ㄎㄎㄎㄎ

Caching

Query treequery { user(login: "zetavg") { name repositories { name } } }

{ "data": { "user": { "name": "Pokai Chang", "repositories": [ { "name": "dotfiles" }, { "name": "Thing" }, { "name": "Stuff" } ] } } }

Query treeQuery Root

User

Repo Repo Repo

name

user(login: "zetavg")

"Pokai Chang"

"dotfiles"

name repositories

name

"Thing"

name

"Stuff"

query { user(login: "zetavg") { name repositories { name } } }

{ "data": { "user": { "name": "Pokai Chang", "repositories": [ { "name": "dotfiles" }, { "name": "Thing" }, { "name": "Stuff" } ] } } }

Caching the query result

⬢ Strategy 1: traverse path

Query Root

user(login: "zetavg")

User

Repo Repo Repo

name

"Pokai Chang"

"dotfiles"

name repos

name

"Thing"

name

"Stuff"

⬡ Same path, same object

⬢ Strategy 1: traverse path

Query Root

user(login: "zetavg")

User

Repo Repo Repo

name

"Pokai Chang"

"dotfiles"

name repos

name

"Thing"

name

"Stuff"

user(login: "zetavg")

user(login: "zetavg")/repos[2]

⬡ Same path, same object

Caching the query result

⬢ Strategy 1: traverse path

Query Root

user(login: "zetavg")

User

Repo Repo Repo

name

"Pokai Chang"

"dotfiles"

name repos

name

"Thing"

name

"Stuff"

repo(owner: "zetavg", name: "dotfiles")

Repo

name

"dotfiles"

⬡ Sometimes path assumption isn’t enough

Caching the query result

⬢ Strategy 1: traverse path

Query Root

user(login: "zetavg")

User

Repo Repo Repo

name

"Pokai Chang"

"dotfiles"

name repos

name

"Thing"

name

"Stuff"

repo(owner: "zetavg", name: "dotfiles")

Repo

name

"dotfiles"

Same object on different path

⬡ Sometimes path assumption isn’t enough

Caching the query result

⬢ Strategy 1: traverse path

⬢ Strategy 0: object identifier

repo/dotfiles

Repo

name

"dotfiles"

repo/dotfiles

Repo

name

"dotfiles"

Query Root

User

Repo Repo

user(login: "zetavg")

"Pokai Chang"

name repos

name

"Thing"

name

"Stuff"

repo(owner: "zetavg", name: "dotfiles")

repo/Thing repo/Stuff

Caching the query result

⬢ Strategy 1: traverse path

⬢ Strategy 0: object identifier

Query Root

User

Repo Repo

user(login: "zetavg")

"Pokai Chang"

name repos

name

"Thing"

name

"Stuff"

repo(owner: "zetavg", name: "dotfiles")

repo/Thing repo/Stuff

Repo

name

"dotfiles"

repo/dotfiles

Caching the query result

⬢ Strategy 1: traverse path

⬢ Strategy 0: object identifier

⬡ Relay: we need the server to give a global id for nodes that need to be identified

⬡ Apollo: client defines a dataIdFromObject function that will be executed on every node

⬡ Fun fact: Relay stores each object it fetched in a key-value store with the object id or traverse path as key, any field that contains an object will actually be the key of the object, so two objects having the same id will be ensured the same by Implementation

Caching the query result

Pagination

Cursor Based Pagination

⬢ Offset based pagination, e.g.: per_page=5&page=1

Cursor?

page 1 page 2 page 3

⬢ Offset based pagination, e.g.: per_page=5&page=1

Cursor?

page 1 page 2 page 3

1 2 3 4 5

page 1

Client fetches page 1

⬢ Offset based pagination, e.g.: per_page=5&page=1

Cursor?

page 1 page 2 page 3

' Broken

page 1 page 2 page 3

1 2 3 4 5

page 1

Data inserted

⬢ Offset based pagination, e.g.: per_page=5&page=1

Cursor?

page 1 page 2 page 3

' Broken

page 1 page 2 page 3

1 2 3 4 5

page 1 page 2

5 6 7 8 9

'

Client got malformed results

⬢ Offset based pagination, e.g.: per_page=5&page=1

⬢ Cursor based pagination, e.g.: after: "…", next: 5

Cursor?

next 5next 5

page 1 page 2 page 3

' Broken

page 1 page 2 page 3

Relay Connections⬢ The design of Relay Cursor Connections

query { viewer { friends(first: 10, after: "someCursor") { edges { cursor node { id name } } pageInfo { hasNextPage } } } }

Edge (UserEdgeType)

Node (UserType)

{ … }

Cursor

Current cursor Connection

Edges

Edge (UserEdgeType)

Node (UserType)

{ … }

Cursor

Edge (UserEdgeType)

Node (UserType)

{ … }

Cursor

Page Info

Starting cursor

Update

Mutations⬢ A mutation is a query that has side effects

⬢ The changes made on the graph will be put on the response, the client is responsible to select the necessary parts

mutation { renameRepo(input: { repoID: "…", name: "NewName" }) { repo { id name } } }

Grab the changes that are

made on the existing repo

Mutations⬢ A mutation is a query that has side effects

⬢ The changes made on the graph will be put on the response, the client is responsible to select the necessary parts

⬢ In general, we need to write an updater function to update the store with the payload:

(oldState, payload) => newState ⬢ Relay and Apollo both has some conventions

⬡ Objects with matching identifier in the store will be updated automatically

Mutations

UI

$ㄎㄎㄎㄎ

dddddd

MutationStore

Mutations

UI

$ㄎㄎㄎㄎ

dddddd

Mutation

Server

Store

Mutations

UI

$ㄎㄎㄎㄎ

dddddd

Mutation

Server

Updater

Store

Response

Mutations

UI

$ㄎㄎㄎㄎ

dddddd

Mutation

Server

Updater

Store

Response

Optimistic Update

Mutations

UI

$ㄎㄎㄎㄎ

dddddd

Mutation

Server

Updater

Store

Latency

Response

'

Optimistic Update

UI

$ㄎㄎㄎㄎ

dddddd

MutationStore

Latency

Optimistic Update

UI

$ㄎㄎㄎㄎ

dddddd

Mutation Optimistic Updater

Store

Latency

Optimistic Update Layer

Optimistic Update

UI

$ㄎㄎㄎㄎ

dddddd

Mutation Optimistic Updater

Server

Updater

Store

Latency

Response

Optimistic Update Layer

Optimistic Update

UI

$ㄎㄎㄎㄎ

dddddd

Mutation Optimistic Updater

Server

Updater

Store

Latency

Response

Realtime UI

GraphQL Live Query⬢ Idea: after the client sends a query, server can push

updates of the query result to the client

⬢ May require a fully reactive backend

⬢ No open implementations yet

GraphQL Subscriptions⬢ Clients can subscribe to a specific type of event as a

similar way as how we do mutations ⬢ Mutations are client-made changes while

Subscriptions are server-pushed updates ⬢ New query results will be pushed to the client when a

event occurred

subscription { todoItemAddedToList(todoListID: "…") { todoItem { name } } }

GraphQL Subscriptions⬢ Clients can subscribe to a specific type of event as a

similar way as how we do mutations ⬢ Mutations are client-made changes while

Subscriptions are server-pushed updates ⬢ New query results will be pushed to the client when a

event occurred ⬢ GraphQL just tells us how things should work, we

need to configure different implementations (WebSocket, APNS, GCM) of sending the data on different platforms

GraphQL Subscriptions

UI

$ㄎㄎㄎㄎ

dddddd

Subscription

Server

Updater

Store

When event occurred

On mount (normally)

References⬢ GraphQL API Explorer

⬢ GraphQL Concepts Visualized

⬢ Mutations and Optimistic UI in Apollo Client

⬢ GraphQL Subscriptions in Apollo Client

⬢ https://github.com/zetavg/graphql-todomvc

Thanks + Q&A

Recommended