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.
This is part 2, you can also find part 3 here. 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
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 frontendcd frontendnpm 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.
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.