GraphQL Dynamic Persisted Queries

In my previous post GraphQL Persisted Queries Using GET Requests we built a GraphQL server that used middleware to pattern match against a static extracted queries file. That extracted queries file housed the query we knew our client would send. As you can imagine, keeping a static file in sync between the server and client can be a pain, this approach just won’t scale. To solve this problem we’ll use a technique called Dynamic Persisted Queries. We will build a react client that dynamically generates a hash and sends that over to our server. We’ll also update our GraphQL server to not only execute the corresponding query, but save the hash in memory for all subsequent requests.

If you haven’t read my previous post I suggest you start there, as this tutorial will pick up right where we left off. For those of you that are up to date with the idea of persisted queries and want to skip the previous article, you can find the project files here: source files.

To jump start our build we’ll need to install some global packages. We’ll use create-react-app to build our react client, and redis for our in-memory db.

npm install -g create-react-app redis-cli

We’ll also need a working copy of redis on our machine, for that we can use Homebrew.

brew install redis

Next, we need to add a redis client and the cors middleware to our GraphQL server.

npm install ioredis cors --save

server.js

In this example we incorporate the cors middleware to handle cross origin requests. This is just for example purposes. It would be much safer to use a proxy of some sort instead.

We require the cors middleware on line 9 and add it to our /graphql route on line 17. We should also update our schemas and resolvers to give us a bit more data to play with.

schemas.js

resolvers.js

Now use create-react-app to create a new working directory for our react client and install the following packages.

create-react-app frontend
cd frontend
npm install --save graphql graphql-tag react-apollo apollo-client apollo-link-http apollo-cache-inmemory apollo-link-persisted-queries react-router react-router-dom

Lets create a queries file with a couple queries for our react client to create hashes for.

src/queries.js

Here we add two queries to our queries.js file using graphql-tag, getGreeting and extendGreeting. The extendGreeting query simply adds a few more fields to the response. We’ll need to create a component to call our home page and two additional components that’ll query our GraphQL server as we navigate from page to page.

src/components/home.js

src/components/pageOne.js

src/components/pageTwo.js

Our home.js component simply returns a string informing us of our location. The pageOne.js and pageTwo.js components both use the Query component from react-apollo to send requests to our GraphQL server. Inside our Query components we pass in the function to be run once we receive a response. If the request is pending we’ll show the text Loading... and if an error occurs we’ll render the error message. If all goes well, we’ll use the data received to display our greeting. To finish up our client we’ll use some very cool packages from Apollo to tie it all together. Let’s open up index.js and replace it with the following.

We begin by instantiating a new client using the apolloClient class from apollo-client. We also need to pass in the required configuration options. We’ll use the InMemoryCache function from apollo-cache-inmemory for the value of our cache property (A required step when using Apollo Client 2.0). Next, for the link property we’ll use the createPersistedQueryLink link from apollo-link-persisted-queries. We pass in the useGETForHashedQueries option and set it to true, this is to ensure we don’t lose our ability to cache on the edge. Next, we’ll concat the createHTTPLink link from apollo-http-link and pass in our GraphQL server endpoint.

To render our app we’ll pass in the ApolloProvider component from react-apollo as an argument to the render function. We pass the client we created above as a prop to ApolloProvider , then use the BrowserRouter component from react-router as a child to configure our routes. Finally we’re ready to fire this bad boy up and display some content in the browser. Open a terminal window in the root directory of both the GraphQL server and our react client, and run the following commands.

GraphQL server

node server.js

React client

npm run start

In the GraphQL server terminal you should see listening on port: 4000 . In the terminal for the react client you should see You can now view frontend in the browser. Open up your browser and navigate to http://localhost:3000.

Ok, here’s where the magic happens. Despite the simplicity of the web page we’ve constructed, as you navigate from page to page you will notice something very cool happening. Our react client is now sending an auto generated hash to our GraphQL server. We have yet to update our persistedQueries.js file but our GraphQL queries are still succeeding. This is due to apollo-link-persisted-queries being smart enough to detect the error codes our GraphQL server is sending back. In this case our GraphQL server is responding with a 400 Bad Request, which our react client recognizes as a total failure and retries the query using a post request (the default approach). Since all changes we’ve made so far are backwards compatible the second request succeeds allowing our pages to render successfully.

Our react client is complete, now all we need to do is jump back over to our GraphQL server and update our persisted queries middleware. Open up persistedQueries.js and replace it with the following

There is a lot going on here so lets break this down step by step. First our persistedQueries middleware calls the method function passing in the request object.

The method function detects whether we are receiving a post request or a get request. If our GraphQL server received a get request then the data we’re looking for arrived as JSON and resides in the query property. If our server received a post request, then the data we need is in the body as a Javascript object.

Next, we destructure the data object in search of the sha256Hash. If the hash doesn’t exists we’ll call next() and send the request on it’s way to the next middleware function(s). If the hash exists we’ll check the status of our redis client. If we were unable to establish a connection to our redis client then we call handleQuery, which will go ahead and process the request without searching for the sha256Hash in our redis database. If our redis connection was successful, then we’ll build our cache key by appending a namespace to the operation name and the sha256Hash. Then we use that cache key to query our redis db. Whether we find a match or not, we’ll call handleQuery to process the rest of the request.

By this time, if the cache key we constructed corresponds to a GraphQL query saved in our db, we’ll go ahead and assign that to the query property on the request object. Then we call next, sending the requested query to be processed by our GraphQL server.

If the cache key never returns a GraphQL query from our redis db, and the request data does not contain a query property (as it does when it’s a post request), then our GraphQL server will send back an error response with a very important message, persistedQueryNotFound. This exact error message is used to inform our apollo-link-persisted-queries package to retry the request using a standard post request. Only this time, the retry will also include the original sha256Hash it sent with the first attempt.

If the request does in fact contain a query property, then we’ll use GraphQL’s validate method to ensure its value is a valid GraphQL query. validate returns an array of all errors found, in our case we check the length of the array and send back an error message if any errors were encountered.

Once we’re confident that our query is valid, we can check the status of our redis client connection. If a connection hasn’t been established we’ll go ahead and just send the query to our GraphQL server. If our redis connection was successful however, we’ll build our cache key and save the query in our db. Now all subsequent requests containing the same sha256Hash will be processed successfully. Time to see all of our hard work payoff. Let’s spin up our redis db.

redis-server

Then restart both apps in their respective terminals and open up your browser.

On the first visit to page one or page two our react client sends a generated hash to our GraphQL server requesting the data it needs. This request, of course, fails since the generated hash is not yet saved in our redis database. Our GraphQL server responds with the proper error message persistedQueryNotFound, prompting our react client to retry the request once again as a post request while including the sha256Hash. The retry is successfully fulfilled and the sha256Hash is saved in our redis db. From this point on, all subsequent requests are fulfilled using only a sha256Hash via get request.

Each successful GET request can be cached on the edge by our CDN

Success! Now we can remove ourextracted_queries.js file, eliminating the burden of keeping a static file in sync between the server and the client. In case you had any trouble following along, you can find the completed project files here: source files.