Persisted GraphQL Queries with Apollo Client
Dhaivat Pandya
Update: This post was originally written by Dhaivat Pandya in February 2017, but we’ve updated it since them (most recently in December 2017) to include the latest information on using persisted queries with Apollo.
If you’re a regular at a diner, you know that saying the full name of a dish is a rookie move. If you refer to the “double cheeseburger with bacon, fries and a coke” as a “#12”, you save yourself a bunch of words and enable the cashier to be more efficient in fulfilling your order.
There’s a popular concept in GraphQL called “persisted queries” which is the same kind of thing, but for your API. We’ve built two different tools in Apollo to make it easy to get started building with persisted queries:
- Automatic persisted queries — a built-in feature when using Apollo Link and Engine, works with any client or server
- persistgraphql — a library to statically analyze GraphQL queries at build time, uses a tight coupling between client and server
This article is going to introduce you to how persisted queries work and how you can get started using them with persistgraphql.
If you’d like to learn more about using automatic persisted queries with the Apollo Link and Engine, check out our blog post on that instead! 🎉
If you’ve built a GraphQL server in the past, you’ve probably thought about one or both of these:
- Restricting the queries that your server accepts by whitelisting a specific set of queries
- Saving bandwidth between the client and server by only sending a query ID or hash instead of an entire GraphQL query
What if we could solve the challenges of query whitelisting and bandwidth usage in one swoop? That’s what persisted queries support does! Persisted queries have been talked about a lot, in Apollo Client, in express-graphql, and in talks by developers at Facebook and in this post, we’ll tell you everything you need to know about them: how they work, the tooling we’ve built around them and how you can incorporate persisted queries into your GraphQL application.
What are persisted queries?
Let’s say that we’re writing some frontend code that uses GraphQL. We might write some view layer code that looks like this:
import COMMENT_QUERY from ‘../graphql/Comment.graphql’;const withData = graphql(COMMENT_QUERY, { options: ({ params }) => ({ … }), … });
where we’re using the graphql-tag/loader Webpack loader in order to import a .graphql
file. When we render this component, Apollo Client will fire off the query contained within Comment.graphql
. To do this, Apollo Client has to send down the query itself to the server as a string and the server will look at the query, resolve it and send back some data. All of this happens at runtime, giving rise to high bandwidth usage and unrestricted query execution.
But if you follow our advice, you already know at build time what query the client is going to send. That is, the query document itself is in the file Comment.graphql
before the client has even rendered. So the client could just tell the server “Hey, can I have the results for query number 16?” and the server, which has agreed on a number-to-query-mapping with the client, will send back the right results. We save ourselves some bandwidth by not sending down the whole document, and the server will only resolve queries that already knows about, preventing an adversary from sending malicious queries.
We use the term “persisted queries” to describe this agreement of a number-to-query-mapping. What it really represents is a bidirectional mapping between GraphQL documents and generated IDs. But, in order for us to be able to implement support for persisted queries, we need to have queries that are statically analyzable, meaning that we should be able to determine the query that the client sends down at buildtime. Apollo Client has adopted static queries from the start and we’ve now built the tooling needed around persisted queries to make them a drop-in addition to your app.
How persisted queries work
Client
In order to support persisted queries, we’ve built <a href="https://github.com/apollographql/persistgraphql" target="_blank" rel="noreferrer noopener">persistgraphql</a>
, a build tool that crawls your client source code repository, pulls out GraphQL documents, creates a mapping between GraphQL documents and their generated IDs and writes that map to a JSON file. It also provides a network interface implementation for Apollo Client that takes in whole queries, but sends only the query ID to the server.
Let’s break open these pieces and see how they actually function, starting with the JSON map itself. You might imagine that the map looks something like this:
{ <GraphQL Document AST>: < ID >, }
Unfortunately, keys in Javascript can only be strings or numbers. We could fix that issue by using graphql-js
’s print
function:
{ <print(GraphQL Document AST) >: < ID >, }
This is nearly right, but Apollo Client allows you to apply some static transformations to queries such as adding the __typename
field to every level of our query. One of our queries might contain the following GraphQL document (we call this the source query):
query { author { firstName lastName } }
After applying a query transformation, the query that’s actually sent to the server (we call this the network query) looks like this:
query { author { firstName lastName __typename } }
Since we want to be able to implement persisted queries support within Apollo Client by simply implementing a network interface, we want our query map to be able to map between these network queries and the generated IDs. So, our final JSON map ends up looking like this:
{ < print(transform(GraphQL Document)) >: < ID >, }
This means that our command line tool has to be able to apply these query transforms at build time. That’s why there’s an option you can pass to persistgraphql
to apply the query transformation that adds typenames and it’s easy to add other query transformations.
Within the network interface for persisted queries, the map data structure that the persistgraphql
tool outputs is made available to the client as part of the bundle. The core of this network interface consists of a single method:
query(request: Request) { // look up request.query in the map data structure // find the corresponding ID // send the query ID to the server, along with the query variables // return a Promise for the result returned by the server }
The request.query
field contains the transformed query that should be sent over the wire. The persistgraphql
persisted query network interface looks up the ID associated with request.query
within the map data structure. Finally, it sends this query ID is sent down to the server, along with any variables needed to evaluate the query. Simple, right?
Server
You can probably already guess what happens on the server. The server will receive something like this from the client:
{ id: < query ID >, variables: < variables >, }
and turn it into this:
{ query: < print(GraphQL document) >, variables: < variables > }
This means that after you drop in some really simple middleware, your server code doesn’t have to change at all since it can just pretend that it received the GraphQL document from the client, per usual.
How to use it
Enough talk about the internals; let’s see how to actually set up your client and server to use persisted queries. We first need to install persistgraphql
:
$ npm install --save persistgraphql
This gives us the persistgraphql
CLI tool, as well as an Apollo Client network interface. We can use this tool to create the JSON file that contains the mapping between queries and IDs. We just need to point persistgraphql
to our frontend source directory that contains our .graphql
files:
$ persistgraphql ui [--add_typename]
where we pass the --add_typename
flag if we’re using the addTypename
transformer within Apollo Client. If all goes well, that should produce a file called extracted_queries.json
. In our client code, we have to import this file and replace our existing network interface with the PersistedQueryNetworkInterface
from persistgraphql/lib/browser
. This might look like this:
import queryMap from ‘./extracted_queries.json’; import { PersistedQueryNetworkInterface } from ‘persistgraphql’;const networkInterface = new PersistedQueryNetworkInterface({ queryMap, uri: apiUrl, opts: { credentials: ‘same-origin’, headers, }, });
Notice that in order for the import statement to work correctly, you need to configure Webpack (or whatever build tool you choose to use) to handle JSON files correctly; you can check out GitHunt-React, our example app, for the details. Finally, on the server, we have to make a similar import (we could also use Node’s filesystem API to load the JSON file) and write a simple middleware to replace incoming query IDs with the whole thing:
import queryMap from ‘../extracted_queries.json’; import { invert } from 'lodash'; app.use( '/graphql', (req, resp, next) => { if (config.persistedQueries) { const invertedMap = invert(queryMap); req.body.query = invertedMap[req.body.id]; } next(); }, );
Of course, there are number of ways to implement the middleware. Here, we’re using invert
from lodash
in order to invert the map and look up by id
, but you could imagine loading queries from a database or some other storage mechanism.
And, that’s it! With these changes in place, the app will start loading data using query IDs rather than entire query documents.
Why This Matters
Persisted queries provide a number of immediate benefits, most importantly query whitelisting and reduced bandwidth usage. Because persisted queries are static by definition, they also give you the possibility of optimizing execution on the server for specific queries, for example by hand-crafting a highly efficient database query.
What’s next
Persisted queries are just the first step in the direction of more production-focused features for Apollo Client. By implementing persisted queries, we’ve shown that with Apollo you can have your cake and eat it too: The features gives you great performance and security optimizations without sacrificing the fantastic developer experience that Apollo is known for. As we go forward, we’ll continue to build features like persisted queries that make GraphQL a fantastic choice for prototypes and production applications alike.
If you’re interested in working on Open Source GraphQL projects: we’re always looking for more contributors. We’re also hiring!