Working With GraphQL Caching
September 23, 2021If you’ve recently started working with GraphQL, or reviewed its pros and cons, you’ve no doubt heard things like “GraphQL doesn’t support caching” or “GraphQL doesn’t care about caching.” And for most, that is a big deal.
The official GraphQL documentation refers to caching techniques so, clearly, the folks behind it do care about caching and its performance benefits.
The perception that GraphQL is at odds with caching is something I want to address in this post. I want to walk through different caching techniques and how to leverage cache for GraphQL queries.
Getting GraphQL to cache automatically
Consider the following query to fetch a post and the post author:
query getPost {
post(slug: "working-with-graphql-caching") {
id
title
author {
id
name
avatar
}
}
}
The one “magic trick” that makes automatic GraphQL caching work is the __typename
meta field that all GraphQL APIs expose.
As the name suggests, __typename
returns the name of the objects type. This field can even be manually added to existing queries — and most of the time, a GraphQL client, or CDN will do it for you. urql is one such GraphQL client. The server might receive a query like this:
query getPost {
post(slug: "working-with-graphql-caching") {
__typename
id
title
author {
__typename
id
name
}
}
}
The response with __typename
might look a little something like this:
{
data: {
__typename: "Post",
id: 5,
title: "Working with GraphQL Caching",
author: {
__typename: "User",
id: 1,
name: "Jamie Barton"
}
}
}
The __typename
is a critical piece of the GraphQL caching puzzle because we can now cache this result, and know it contains the Post ID 5 and User ID 1.
Then there are libraries like Apollo and Relay, that also have some level of built-in caching we can use for automatic caching. Since they already know what’s in the cache, they can defer to the cache instead of remote APIs to fetch what the client asks for in a query.
Automatically invalidate the cache when there are changes
Imagine the post author edits the post’s title with the editPost
mutation:
mutation {
editPost(input: { id: 5, title: "Working with GraphQL Caching" }) {
id
title
}
}
Since the GraphQL client automatically adds the __typename
field, the result of this mutation immediately tells the cache that Post ID 5 has changed, and any cached query result containing that post needs to be invalidated:
{
data: {
__typename: "Post",
id: 5,
title: "Working with GraphQL Caching"
}
}
The next time a user sends the same query, the query fetches the new data from the origin rather than serving the stale result from the cache. Magic!
Normalized GraphQL caching
Many GraphQL clients won’t cache entire query results.
Instead, they normalize the cached data into two data structures; one that associates each object with its data (e.g. Post #5: { … }
, User #1: { … }
, etc.); and one that associates each query with the objects it contains (e.g. getPost: { Post #5, User #1}
, etc.).
See urql’s documentation on normalized caching or Apollo’s “Demystifying Cache Normalization” for specific examples and use cases.
Caching edge cases with GraphQL
The one major edge case that GraphQL caches are unable to handle automatically is adding items to a list. So, if a createPost
mutation passes through the cache, it doesn’t know which specific list to add that item to.
The easiest “fix” for this is to query the parent type in the mutation if it exists. For example, in the query below, we query the community
relation on post
:
query getPost {
post(slug: "working-with-graphql-caching") {
id
title
author {
id
name
avatar
}
# Also query the community of the post
community {
id
name
}
}
}
Then we can also query that community from the createPost
mutation, and invalidate any cached query results that contain that community:
mutation createPost {
createPost(input: { ... }) {
id
title
# Also query and thus invalidate the community of the post
community {
id
name
}
}
}
While it’s imperfect, the typed schema and __typename
meta field are the keys that make GraphQL APIs ideal for caching.
You might be thinking by now that all of this is a hack and that GraphQL still doesn’t support traditional caching. And you wouldn’t be wrong. Since GraphQL operates via a POST
request, you’ll need to offload to the application client and use the “tricks” above to take advantage of modern browser caching with GraphQL.
That said, this sort of thing isn’t always possible to do, nor does it make a lot of sense for managing cache on the client. GraphQL clients make you manually update the cache for even trickier cases, but a service like GraphCDN provides a “server-like” caching experience, which also exposes a manual purging API that lets you do things like this for greater cache control:
# Purge all occurrences of a specific object
mutation {
purgeUser(id: [5])
}
# Purge by query name
mutation {
_purgeQuery(queries: [listUsers, listPosts])
}
# Purge all occurrences of a type
mutation {
purgeUser
}
Now, no matter where you consume your GraphCDN endpoint, there’s no longer a need to re-implement cache strategies in all of your client-side logic on mobile, web, etc. Edge caching makes your API feel super fast, and reduces load by sharing the cache amongst your users, and keeping it away from each of their clients.
Having recently used GraphCDN on a project, it’s taken care of configuring a cache on the client or server for me, allowing me to get on with my project. For instance, I can swap out my endpoint with GraphCDN and obtain query complexity (which is coming soon), analytics, and more for free.
So, does GraphQL care about caching? It most certainly does! Not only does it provide some automatic caching methods baked right into it, but a number of GraphQL libraries offer additional ways to do it and even manage it.
Hopefully this article has given you some insight into the GraphQL caching story, and how to go about implementing it on the client, as well as taking advantage of CDNs to do it all for you. My goal here isn’t to sell you on using GraphQL on all your projects or anything, but if you are choosing between query languages and caching is a big concern, know that GraphQL is more than up for the task.