With
Dave Anderson
@dvndrsn
Meet your neighbor!
In this tutorial!
What is GraphQL?
GraphQL is a query language for your API
Specification, implemented in many languages and frameworks.
Provides a system of constraints that helps us drive a constantly evolving API.
In this tutorial!
Our focus today
What is GraphQL and what are its benefits?
How do I get started with GraphQL in Python?
What does it mean for an API to be Relay-compliant?
Why GraphQL?
Ask for what you need, get exactly that
Avoid data over-fetching by clients
Easy for clients to express precise field-by-field data requirements, lazily evaluated on the server
Encourages concentration of business logic and domain model in API and backend
Keep clients lean, focused on presentation of data
Easier to pivot or add a new platform in the future
One request for all your data needs
Avoid N+1 requests for resources in clients
Most data can be fetched on page load in one request or catered to specific components or user interactions
Single endpoint increases discoverability and avoids versioning more endpoints than a squid has arms
Why GraphQL?
Describe what's possible with a type system
GraphQL specification requires a schema, every field defined and typed
Why GraphQL?
Evolve your API without versions
GraphQL schema provides a safe guide for evolving over time
Confidence in making changes encourages building only what you need now
Embrace You-Ain't-Gonna-Need-It
Evolve as needs become more
clear
Provides first class tools for deprecation and controlled changes to APIs
Why GraphQL?
GraphQL!
Move faster with powerful developer tools
Static schema typing and runtime introspection enables tooling to help you move faster
IDE-like editors for rapid prototyping of queries for clients
Robust clients manage concerns like caching and asynchronous actions letting you focus on the code that matters
GraphQL!
Bring your own data and code
Broad support across languages.
Python community growing!
GraphQL!
Unifying API for any kind of data source
Leverage as glue for other data
sources and API
Backend for frontend for
everything. One size
fits most.
Logistics!
Workshop schedule!
Logistics!
Principles!
Logistics!
Branch Checkouts!
Project setup - see README.md.
# 0. Install Python (Target 3.6+), `invoke` and git
# 1. Clone repo
$ git clone https://github.com/dvndrsn/graphql-python-tutorial.git
$ cd graphql-python-tutorial
# 2. Checkout Chapter 1
$ git checkout chapter-1
# 3. Setup dependencies (pipenv, graphene, django, etc.) and fixture data (sqlite) using `invoke`
$ pip install invoke
$ invoke setup
# 4. Check setup - lint and test code
$ invoke check
# 5. Start Django Server
$ invoke start
# 6. Open GraphiQL - in your web browser
$ xdg-open http://localhost:8000/graphql
Logistics!
Dependencies and References!
Version control
# checkout a chapter branch
$ git checkout chapter-1
# save work in progress
$ git add .
$ git commit -m 'my cool commit message here'
# If you get into a state where things seem too hard to correct ...
# get rid of work in progress
$ git reset --hard
# git rid of work in progress and reset chapter the original state
$ git reset --hard origin/HEAD
# stash work in progress (temporary save)
$ git stash
# apply last stash
$ git stash pop
Dependencies and References!
Python environment
# Most of our build commands use `pipenv run` to execute in the context
# of the active virtual environment.
# install dependencies
$ pipenv install
# "activate" virtual environment in terminal shell
$ pipenv shell
# "deactive" virtual environment in terminal shell
(graphql-python-tutorial) $ exit
# Run a predefined scripts in virtual environment
$ pipenv run setup
# OR
$ pipenv run <script> # See Pipfile [scripts] for more.
Dependencies and References!
Task Runner & Build scripts
# list of build commands
$ invoke -l
# check style, types and tests
$ invoke check
# just run the tests
$ invoke test
# OR
$ pipenv run test
# run the django web server
$ make start
# OR
$ pipenv run start
# run a django shell to play with some code
$ invoke shell
# OR
$ pipenv run django_shell
Dependencies and References!
Linting and tests
# sample function annotated for MyPy
# ...
@staticmethod
def resolve_author_name(
root: models.Story, # argument: type
info: graphene.ResolveInfo,
display: str,
) -> str: # -> return_type
return root.author.full_name(display)
# ...
Dependencies and References!
Web framework
# Example ORM commands for reading data
# Select a single author
author = Author.objects.get(pk=2)
# Quereyset - Select all Authors
all_authors = Author.objects.all()
# Queryset - Select only stories with matching criteria
stories = Story.objects.filter(author__in=author)
first_story = stories[0]
# Many-to-one - access model across foreign key
first_story.author
# One-to-many - access model across foreign key
author.stories.all()
Dependencies and References!
GraphQL + Python
Schema, Query and Response
Let's start with a small API, just one resource for listing stories.
Schema, Query and Response
To get story titles and authors with REST...
Schema? Documented in Swagger? README?
{
"data": [{
"id": "1"
"title": "Game of Thrones"
"subtitle": "A Song of Ice and Fire - Book 1",
"description": "Some really long text here...",
"authorName": "George R. R. Martin",
"passageCount": 52,
"averageRating": 4.7,
"links": [{
"href": "1/passages",
"rel": "passages",
"type" : "GET"
}]
}, {
"id": "2",
"title": "Romeo and/or Juliet",
"subtitle": "A Choosable Path Adventure",
"description": "Some really long text here...",
"authorName": "Ryan North",
"passageCount": 300,
"averageRating": 4.5,
"links": [{
"href": "2/passages",
"rel": "passages",
"type" : "GET"
}]
}]
}
GET /api/v1/stories?display=FIRST_LAST
Schema, Query and Response
Now let's query for story titles and authors with GraphQL...
type Query {
stories: [StoryType]
}
enum AuthorDisplayNameEnum {
FIRST_LAST
LAST_FIRST
}
type StoryType {
id: ID!
title: String
subtitle: String
passageCount: Int
averageRating: Float
authorName(
display: AuthorDisplayNameEnum = FIRST_LAST
): String
}
query myFirstQuery {
stories {
id
title
authorName(display:LAST_FIRST)
}
}
{
"data": {
"stories": [{
"id": "1"
"title": "Game of Thrones"
"authorName": "Martin, George R. R."
}, {
"id": "2"
"title": "Romeo and/or Juliet"
"authorName": "North, Ryan"
}]
}
}
type Query {
stories: [StoryType]
}
enum AuthorDisplayNameEnum {
FIRST_LAST
LAST_FIRST
}
type StoryType {
id: ID!
title: String
subtitle: String
passageCount: Int
averageRating: Float
authorName(
display: AuthorDisplayNameEnum = FIRST_LAST
): String
}
Schema Definition Language is a way to describe our API...
Schema, Query and Response
Write queries against that schema..
query myFirstQuery {
stories {
id
title
authorName(display:LAST_FIRST)
}
}
Schema, Query and Response
query myFirstQuery {
stories {
id
title
authorName(display:LAST_FIRST)
}
}
When we execute the query, we get JSON results.
{
"data": {
"stories": [{
"id": "1"
"title": "Game of Thrones"
"authorName": "Martin, George R. R."
}, {
"id": "2"
"title": "Romeo and/or Juliet"
"authorName": "North, Ryan"
}]
}
}
Schema, Query and Response
If you need to change data, write a mutation!
mutation newStory {
createStory (input: {
title: "GraphQL",
author: "Dave A",
description: "Its pretty cool"
}) {
story{
id
title
description
author
}
}
}
Schema, Query and Response
input CreateStoryInput {
title: String
author: String
description: String
}
type CreateStoryPayload {
story: StoryType
}
type Mutation {
createStory(input: CreateStoryInput): CreateStoryPayload
}
{
"data": {
"createStory": {
"story": {
"id": 1,
"title": "GraphQL",
"description": "It's pretty cool",
"author": "Dave A"
}
}
}
}
Define a schema...
GraphiQL is a tool for exploring your API.
Exploring GraphQL
Exploring GraphQL
It provides powerful features to rapidly prototype queries to your API.
GraphiQL POWER USER TIPS
Ctrl + Space: Auto-completion hint.
Can be used for fields, arguments, inputs, enum values.
Ctrl + Enter: Run query under cursor.
Bonus: can autocomplete some fields in selection set for object types.
Exploring GraphQL
Exercise
Exploring GraphQL
Increasing diversity in GraphQL Python ecosystem with code-first and schema-first approaches.
GraphQL + Python
Graphene Django provides a View for parsing GraphQL requests.
# cyoa/urls.py
urlpatterns = [
...
path(
'graphql/',
GraphQLView.as_view(schema=schema, graphiql=True)
),
...
]
GraphQL + Python
Schema is defined through Graphene.
class StoryType(graphene.ObjectType):
id = graphene.ID()
title = graphene.String()
subtitle = graphene.String()
author_name = graphene.String(
args={
'display': graphene.Argument(
AuthorDisplayNameEnum,
default_value=AuthorDisplayNameEnum.FIRST_LAST,
)
}
)
class Query(graphene.ObjectType):
stories = graphene.List(StoryType)
...
schema = graphene.Schema(query=Query)
type Query {
stories: [StoryType]
}
type StoryType {
id: ID!
title: String
subtitle: String
authorName(display:
AuthorDisplayNameEnum = FIRST_LAST
): String
}
GraphQL + Python
class StoryType(graphene.ObjectType):
subtitle = graphene.String()
author_name = graphene.String(
# ...
)
@staticmethod
def resolve_subtitle(
root: models.Story,
info: graphene.ResolveInfo
) -> str:
return root.subtitle
@staticmethod
def resolve_author_name(
root: models.Story,
info: graphene.ResolveInfo,
display: str
) -> str:
return root.author.full_name(display)
GraphQL + Python
Each field in our schema has a resolver function.
Resolver functions help us answer Queries.
class StoryType(graphene.ObjectType):
# ...
@staticmethod
def resolve_author_name(
root: models.Story,
info: graphene.ResolveInfo,
display: str
) -> str:
return root.author.full_name(display)
class Query(graphene.ObjectType):
# ...
@staticmethod
def resolve_stories(
root: None,
info: graphene.ResolveInfo
) -> Iterable[models.Story]:
return Story.objects.all()
query myFirstQuery {
stories {
id
title
authorName(
display:LAST_FIRST
)
}
}
GraphQL + Python
Let's trace data through an example...
schema = graphene.Schema(query=Query)
class Query(graphene.ObjectType):
stories = graphene.List(StoryType)
@staticmethod
def resolve_stories(
root: None,
info: graphene.ResolveInfo
) -> Iterable[models.Story]:
return Story.objects.all()
class StoryType(graphene.ObjectType):
# ...
author_name = graphene.String(...)
@staticmethod
def resolve_author_name(
root: models.Story,
info: graphene.ResolveInfo,
display: str
) -> str:
return root.author.full_name(display)
GraphQL + Python
query
stories
story1
story2
title
id
authorName
title
id
authorName
None
Fields
Root Value
Fields
Root Value
Fields
Exercise
GraphQL + Python
type StoryType {
# Add fields
description: String
publishedYear: String
}
Why Relay compliance?
Relay Compliance
What does it mean for an API to be
Relay Compliant?
A mechanism for re-fetching an object.
A standard way to page through connections.
Structure around mutations to make them predictable.
Relay Compliance
A mechanism for re-fetching an object.
Relay: Node
type Query {
stories: [StoryType]
node(id: ID!): Node
}
An interface in GraphQL defines a set of fields which are shared across types that implement the interface.
Relay: Node
interface Node {
id: ID!
}
type StoryType implements Node {
# ...
}
class StoryType(graphene.Object):
class Meta:
interfaces = (graphene.Node,)
# ...
Node Interface
Disambiguating objects with Interface or Union
Relay: Node
class StoryType(graphene.Object):
# ...
@classmethod
def is_type_of(
cls,
root: Any,
info: graphene.ResolveInfo
) -> bool:
return isinstance(root, models.Story)
Querying the node field!
Relay: Node
query storyInfo($storyId: ID!) {
story: node(id: $storyId) {
id
... on StoryType {
title
description
}
}
}
# Variables:
# {"storyId": "U3RvcnlUeXBlOjI="}
The node ID value is meant to be
Relay: Node
U3RvcnlUeXBlOjI=
StoryType:2
<GraphQL-Type>:<ID>
Graphene base-64 encodes ID values:
The node Field returns any type that implements Node matching the provided ID.
Relay: Node
type Query {
# ...
node(id: ID!): Node
}
class Query(graphene.Object):
# ...
node = graphene.Node.Field()
The graphene Node field defers to the correct type to load data.
Use the decoded id in get_node class method
Relay: Node
class StoryType(graphene.Object):
# ...
@classmethod
def get_node(
cls,
info: graphene.ResolveInfo,
id_: str
) -> Story:
pk = int(id_)
return Story.objects.get(pk=pk)
Relay: Node
Exercise
query dataForOneStory($storyId: ID!) {
story: node(id: $storyId) {
id
... on StoryType {
title
description
}
}
}
# Variables:
# {"storyId": "U3RvcnlUeXBlOjI="}
A standard way to page through connections.
Relay: Connection
type Query {
stories: [StoryType]
stories(first: Int, after: String, last: int, before: String): StoryConnection
}
Forward pagination using a connection field.
Relay: Connection
query($afterCursor: String) {
stories(first:10, after: $afterCursor) {
pageInfo {
endCursor
hasNextPage
}
edges {
cursor
node {
id
title
subtitle
description
authorName(display: FIRST_LAST)
}
}
}
}
# Variables : {
# "afterCursor": "YXJyYXljb25uZWN0aW9uOjA="
# }
Relay Connections use a Cursor-based pagination API
Relay: Connection
Graphene uses Cursor based API with Limit/Offset under the hood.
Relay: Connection
YXJyYXljb25uZWN0aW9uOjA=
arrayconnection:0
<pagination-style>:<offset>
The Connection API for cursor based pagination provides.
Relay: Connection
type Query {
stories(
first: int,
after: String,
last: int,
before: String
): StoryConnection
}
type StoryConnection {
pageInfo: PageInfo
edges: [StoryEdge]
}
type PageInfo {
endCursor: String
hasNextPage: Boolean!
startCursor: String
hasPreviousPage: Boolean!
}
type StoryEdge {
cursor: String!
node: StoryType!
}
class StoryType(graphene.Object):
# ...
class StoryConnection(graphene.Connection):
class Meta:
node = StoryType
class Query(graphene.ObjectType):
stories = graphene.ConnectionField(StoryConnection)
# ...
Moving from List to Connection is a breaking change for our API consumers!
Relay: Connection
Relay: Connection
Exercise
query pageForwardThroughStories($afterCursor: String) {
stories(first:3 , after: $afterCursor) {
pageInfo { endCursor hasNextPage }
edges {
cursor
node {
id
title
subtitle
description
authorName(display: FIRST_LAST)
}
}
}
}
# Variables:
# {"afterCursor": null}
Structure around mutations to make them predictable.
Relay: Mutations
type Mutation {
createStory(
input: CreateStoryInput
): CreateStoryPayload
}
input CreateStoryInput {
# ...
clientMutationId: String
}
type CreateStoryPayload {
# ...
clientMutationId: String
}
Structure around mutations to make them predictable
Relay: Mutations
mutation newStory {
createStory (input: {
title: "GraphQL",
author: "Dave A",
description: "Its pretty cool",
clientMutationId: "Mutation-1"
}) {
story{
id
title
description
author
}
}
}
{
"data": {
"createStory": {
"story": {
"id": 1,
"title": "GraphQL",
"description": "It's pretty cool",
"author": "Dave A"
},
"clientMutationId": "Mutation-1"
}
}
}
Graphene provides a simple implementation.
Relay + Mutations
input CreateStoryInput {
title: String!
author: String!
description: String!
clientMutationId: String
}
type CreateStoryPayload {
story: StoryType
clientMutationId: String
}
type Mutation {
createStory(
input: CreateStoryInput
): CreateStoryPayload
}
class CreateStory(graphene.ClientIDMutation):
class Input:
title = graphene.String()
subtitle = graphene.String()
description = graphene.String()
story = graphene.Field('api.query.story.StoryType')
@classmethod
def mutate_and_get_payload(
cls,
root: None,
info: graphene.ResolveInfo,
**input_data: dict
) -> 'CreateStory':
# use input_data to create `story`
# ...
return cls(story=story)
In review
Relay Compliance
Making changes to a public interface can be hard!
Schema Evolution vs. Versioning
Evolution without breaking changes.
Schema Evolution vs. Versioning
Base 64 encoding tokens like ID and Pagination Cursor encourage client safety too.
Schema Evolution vs. Versioning
Adding a new field
Schema Evolution vs. Versioning
Adding a new argument
Schema Evolution vs. Versioning
Evolving Any API
Schema Evolution vs. Versioning
Tooling
Schema Evolution vs. Versioning
Breaking Changes
Schema Evolution vs. Versioning
Versioning
Schema Evolution vs. Versioning
Let's add a new related record for Story, Author to our API.
Schema Evolution vs. Versioning
In REST, we might add a separate endpoint /v1/authors:
Schema Evolution vs. Versioning
Add Author type and authors field to the root query.
Schema Evolution vs. Versioning
type Query {
...
authors(before: String, after: String, first: Int, last: Int): AuthorConnection
}
type AuthorType implements Node {
id: ID!
firstName: String
lastName: String
fullName(display: AuthorDisplayNameEnum = FIRST_LAST): String
twitterAccount: String
# ...
}
What should our focus be as we grow the project structure?
api/
query/
__init__.py
author.py
story.py
base.py
mutation/
__init__.py
author.py
story.py
base.py
__init__.py
schema.py
# domain models and logic separate from api
application/...
Evolving Graphene Schema & Project Structure
Multiple inheritance for base Query and Mutation in package
Evolving Graphene Schema & Project Structure
# api/query/base.py
class Query(
StoryQuery,
AuthorQuery
):
pass
# api/mutation/base.py
class Mutation(
StoryMutation,
AuthorMutation
):
pass
Let's evolve the schema!
type Query {
...
authors(before: String, after: String, first: Int, last: Int): AuthorConnection
}
type AuthorType implements Node {
id: ID!
firstName: String
lastName: String
fullName(display: AuthorDisplayNameEnum = FIRST_LAST): String
twitterAccount: String
# ...
}
Evolving Graphene Schema & Project Structure
Add fields that connect the two types.
Schema Evolution vs. Versioning
type AuthorType implements Node {
# ...
stories(before: String, after: String, first: Int, last: Int): StoryConnection
}
type StoryType {
author: AuthorType
# ...
}
Depreciate fields that are no longer required.
Schema Evolution vs. Versioning
type StoryType {
authorName(display: AuthorDisplayNameEnum = FIRST_LAST): String @deprecated(
reason: "Use StoryType.author.fullname"
)
# ...
}
Let's evolve the schema (even more)!
type AuthorType implements Node {
# ...
stories(before: String, after: String, first: Int, last: Int): StoryConnection
}
type StoryType {
author: AuthorType
authorName(display: AuthorDisplayNameEnum = FIRST_LAST): String @deprecated(
reason: "Use StoryType.author.fullname"
)
# ...
}
Evolving Graphene Schema & Project Structure
In review..
Evolving Graphene Schema & Project Structure
Scaling and Performance
Over time our API may grow into something surprising.
Scaling and Performance
Performance tuning can be more challenging with GraphQL.
Scaling and Performance
What does a simple N+1 GraphQL query look like?
query storiesWithAuthor {
stories(first: 3) {
edges {
node {
id
title
author {
firstName
}
}
}
}
}
S
A
S
A
S
A
S
A
S
A
S
A
S
A
{
"data": {
"stories": {
"edges": [
{
"node": {
"id": "U3RvcnlUeXBlOjI=",
"title": "Romeo and/or Juliet",
"author": { "firstName": "Ryan" }
}
},
{
"node": {
"id": "U3RvcnlUeXBlOjM=",
"title": "User Story Mapping",
"author": { "firstName": "Jeff" }
}
},
{
"node": {
"id": "U3RvcnlUeXBlOjQ=",
"title": "Ancillary Justice",
"author": { "firstName": "Ann" }
}
},
...
]
}
}
}
Scaling and Performance
This can quickly get out of hand with a more nested query...
Stories
Story 1 passages
Story 2 -author, passages, etc..
Passage 1 - Choices
Choice 1 - To Passage
Passage 2 - Choices, etc..
Etc..
Time
query allStories {
stories {
id
title
author {
name
}
passages {
id
description
choices {
id
description
toPassage {
id
description
}
}
}
}
}
Story 1 author
Try a few queries yourself on the chapter-5 branch, monitor the logs for N+1 queries!
Scaling and Performance
To optimize, we can try to aggressively select related data in our queries..
class StoryType(graphene.ObjectType):
...
def resolve_passages(self, info, **kwargs):
return Passage.objects.filter(story=self.id) \
.prefetch_related('to_choices__to_passage')
...
class Query(graphene.ObjectType):
...
def resolve_story(self, info, **kwargs):
return Story.objects.all() \
.select_related('author')
...
query allStories {
stories {
id
title
author {
name
}
passages {
id
description
choices {
id
description
toPassage {
id
description
}
}
}
}
}
Scaling and Performance
Less queries, but risk of overfetching from database!
Stories + author
Story 1 passages +
choices + to passage
Story 2 passages +
choices + to passage
Etc..
Etc..
Time
query allStories {
stories {
id
title
author {
name
}
passages {
id
description
choices {
id
description
toPassage {
id
description
}
}
}
}
}
query allStoriesOverview {
stories {
id
title
passages {
id
description
}
}
}
Scaling and Performance with DataLoaders
Using DataLoader is the Facebook-preferred approach.
DataLoaders:
class PassagesFromStoryLoader(DataLoader):
def batch_load_fn(self, story_ids):
return Promise.resolve(self.get_passages(story_ids))
def get_passages(self, story_ids):
passages = Passage.objects \
.filter(story_id__in=story_ids)
lookup = defaultdict(list)
for passage in passages:
lookup[passage.story_id].append(passage)
return [lookup[story_id] for story_id in story_ids]
class StoryType(graphene.ObjectType):
...
def resolve_passages(self, info, **kwargs):
return info.context.loaders.passages_from_story \
.load(self.id)
...
Scaling and Performance with DataLoaders
Revisiting our simple query performance..
Scaling and Performance with DataLoaders
S
A
S
A
S
A
S
A
S
A
S
A
S
A
query storiesWithAuthor {
stories(first: 3) {
edges {
node {
id
title
author {
firstName
}
}
}
}
}
Batching and caching helps scale performance!
Stories
Passages (batched across Stories)
Choices (batched across Passages)
To Passage (cached)
Time
query allStories {
stories {
id
title
author {
name
}
passages {
id
description
choices {
id
description
toPassage {
id
description
}
}
}
}
}
Author (cached, batched across Stories)
Try a few queries yourself on the chapter-5b branch, monitor for batched queries!
Scaling and Performance with DataLoaders
GraphQL's full potential is more realized with a rich client, like a single page javascript app.
Building a client web application
Apollo is a powerful framework that allows you to focus on writing application code.
Building a client web application
const Stories = () => (
<Query query={STORIES_QUERY}>
{({ loading, error, data }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
return (
<div>
{data.stories.edges.map(({ node: story }) => (
<div key={story.id}>
{`${story.title} (${story.publishedYear})`} -
{story.author.fullName}
<div/>
))}
</div>
);
}}
</Query>
);
const client = new ApolloClient({
uri: "http://localhost:8000/graphql/"
});
const App = () => (
<ApolloProvider client={client}>
<Stories />
</ApolloProvider>
);
const STORIES_QUERY = gql`
query storiesWithAuthor {
stories {
edges {
node {
id
title
publishedYear
author {
id
fullName(
display: FIRST_LAST
)
}
}
}
}
}
`;
Building a client web application
const AddStoryButton = ({ authorId }) => (
<Mutation mutation={ADD_STORY}>
{addStory => {
return (
<div
onClick={() =>
addStory({
variables: {
title: faker.company.companyName(),
subtitle: faker.company.catchPhrase(),
description: faker.company.bs(),
publishedYear: faker.date.past()
.getFullYear(),
authorId: authorId
}
})
}
>
Add story
</div>
);
}}
</Mutation>
);
const ADD_STORY = gql`
mutation addStory(
$title: String
$subtitle: String
$description: String
$publishedYear: String
$authorId: ID
) {
createStory(
input: {
title: $title
subtitle: $subtitle
description: $description
publishedYear: $publishedYear
authorId: $authorId
}
) {
story {
id
title
subtitle
description
publishedYear
}
}
}
`;
Building a client web application
{
"data": {
"stories": {
"edges": [
{
"node": {
"id": "U3RvcnlUeXBlOjQ=",
"title": "Ancillary Justice",
"publishedYear": "2013",
"author": {
"id": "QXV0aG9yVHlwZTo2",
"fullName": "Ann Leckie"
}
}
},
{
"node": {
"id": "U3RvcnlUeXBlOjU=",
"title": "Ancillary Sword",
"publishedYear": "2014",
"author": {
"id": "QXV0aG9yVHlwZTo2",
"fullName": "Ann Leckie"
}
}
},
# ...
]
}
}
}
query storiesWithAuthor {
stories {
edges {
node {
id
title
publishedYear
author {
id
fullName(
display: FIRST_LAST
)
}
}
}
}
}
Building a client web application
Normalization & Caching
query storiesForAuthor {
author(id: "1") {
id
firstName
lastName
stories {
id
title
}
}
}
query listOfAuthors {
authors {
id
firstName
lastName
}
}
mutation changeStory {
updateStory(input:{
id:"1",
title: "Update this!"
}) {
story {
id
title
}
}
}
...we ensure that the cache will be updated properly and updated data flows to components.
As we add more queries and mutations, If all objects have a ID...
Building a client web application
Apollo Development Tools
Building a client web application
API Design for Client Side Caching
As your single page app grows, you may see more caching bugs..
When removing items from lists...
API Design for Client Side Caching
query storiesForAuthors {
author(id: "1") {
id
firstName
lastName
stories {
id
title
}
}
}
mutation removeStory {
removeStory(input:{
authorId:"1",
storyId: "6"
}) {
story { # RETURNS REMOVED STORY DATA
id
title
}
}
}
...you may not see them properly removed on the page.
<Mutation
mutation={REMOVE_STORY}
update={(cache, { data: { removeStory } }) => {
const { stories } = cache.readQuery({ query: GET_STORIES });
cache.writeQuery({
query: GET_STORIES,
data: {
stories: stories.filter(
(stories) => story.id == removeStory.story.id
) },
});
}}
>
...
API Design for Client Side Caching
Apollo provides a mechanism for manually updating the cache...
...but this can be hard to reason about and test.
<Mutation
mutation={REMOVE_STORY}
refetchQueries={['allStories']}
>
...
API Design for Client Side Caching
We could also more simply just refetch any affected queries...
...but as your page grows, it can be hard to maintain a full list of queries and can be expensive to refetch!
query storiesForAuthors {
author(id: "1") {
id
firstName
lastName
stories {
id
title
}
}
}
mutation removeStory {
removeStory(input:{
authorId:"1",
storyId:"6"
}) {
story {
id
title
}
author {
id
stories {
id
}
}
}
}
API Design for Client Side Caching
...the cached list of stories for our author will be updated automatically!
If we design our mutation payload correctly..
query storiesForAuthors {
author(id: "1") {
id
firstName
lastName
stories {
id
title
description
}
}
}
mutation addStory {
addStory(input:{
authorId:"1",
title: "Stuff"
}) {
story {
id
title
}
author {
id
stories {
id
}
}
}
}
API Design for Client Side Caching
When adding to a list with a mutation...
We'll face errors in Apollo due to missing fields in our new post.
query storiesForAuthors {
author(id: "1") {
id
firstName
lastName
story {
...allStoryFields
}
}
}
fragment allStoryFields on Story {
id
title
description
}
mutation removePost {
addStory(input:{
authorId:"1",
title: "Stuff"
}) {
story {
...allStoryFields
}
author {
id
stories {
id
}
}
}
}
API Design for Client Side Caching
Fragments can help us ensure that all fields are fetched when adding an item to a list.
API Design for Client Side Caching
Let's fix some caching bugs!
A parting word before the next adventure
Evolution in a nutshell...
Thanks for participating!
Questions?
@dvndrsn
The Rabbit Hole Podcast: http://bit.ly/2Jous2O
Stride Consulting: https://www.stridenyc.com/careers
Dave Anderson
Thanks for listening!
Icons8 (open source icons):
Open Clip Art:
VectEasy:
Code Sample:
https://github.com/dvndrsn/graphql-python-tutorial
Graphene (Python):
http://docs.graphene-python.org/en/latest/
Apollo (JavaScript):