Learn how to reduce the size of network requests from the Apollo client in the front-end to the GraphQL Ruby back-end with the help of persisted queries. In this article, we will show how these queries work and set them up both on a client and a server with the help of our graphql-ruby-persisted_queries Ruby gem.
One of the benefits of using GraphQL is its flexibility: back-end describes the data model using types, so front-end can only get the data it needs. The problem is that the amount of data required to render a page in a real-world application can be significant, and the query to fetch this data will get out of hand pretty quick. What could help here?
Read how Relay deals with persisted queries here.
First of all, the amount and the variety of often-used queries in your application are limited: usually, front-end knows precisely what it needs from the back-end for every particular view. Popular GraphQL frameworks like Apollo and Relay use that fact to persist queries in the back-end so that a front-end can send just a unique ID over the wire, instead of the full query in all its verboseness. This article will focus on the Apollo implementation on persisted queries.
If you have the Pro Version of
graphql-ruby
–you already have built-in support for persisted queries.
Most of Ruby applications that implement GraphQL server use a gem called graphql-ruby, in this article we are going to find out how to make it work with persisted query IDs coming from the Apollo Client. By default, graphql-ruby
uses POST requests, as query strings tend to become very long, and it makes it hard to configure HTTP caching. If we switch to persisted queries, we will be able to turn on GET requests too and unleash the power of HTTP caching!
The idea behind persisting queries is pretty straightforward–the back-end should look for a special query parameter, containing the unique ID of the query, find the query in the store (we'll talk about it later, imagine that it's just a key-value store) and use it to prepare the response.
Relay has a compiler, and it knows all the queries in the app, so it can put them to a dedicated text file along with their md5 hashes when --persist-output
option is provided. You can save these queries to your store, and you're done: back-end is ready to consume IDs instead of full queries.
Apollo has no compiler, and there is no built-in way to get the list of queries. In this case, queries should be saved to a store at runtime, and there is a special apollo-link-persisted-queries library that enables this feature. First, change your frontend configuration like so:
import { createPersistedQueryLink } from "apollo-link-persisted-queries";
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
import ApolloClient from "apollo-client";
const link = createPersistedQueryLink().concat(createHttpLink({ uri: "/graphql" }));
const client = new ApolloClient({
cache: new InMemoryCache(),
link: link,
});
Now, let's talk about the back-end. With the configured link, client will make an attempt to send the unique ID (sha256 is used in this case) in the extension
param, e.g.:
{
extensions: {
persistedQuery: {
version: 1,
sha256Hash: "688787d8ff144c502c7f5cffaafe2cc588d86079f9de88304c26b0cb99ce91c6"
}
}
}
When server finds the ID in the store—it serves the query if it were sent in full, otherwise–server returns { errors: [{ message: "PersistedQueryNotFound" }] }
in the response payload. In this case client will send the full query along with unique ID and back-end will persist it to the store.
Now we need to implement our back-end functionality in the GraphqlController
:
class GraphqlController < ApplicationController
PersistedQueryNotFound = Class.new(StandardError)
def execute
query_str = persisted_query || params[:query]
result = GraphqlSchema.execute(
query_str,
variables: ensure_hash(params[:variables]),
context: {},
operation_name: params[:operationName]
)
render json: result
rescue PersistedQueryNotFound
render json: { errors: [{ message: "PersistedQueryNotFound" }] }
end
private
def persisted_query
return if params[:extensions].nil?
hash = ensure_hash(params[:extensions]).dig("persistedQuery", "sha256Hash")
return if hash.nil?
if params[:query]
store.save_query(hash, params[:query])
return
end
query_str = store.fetch_query(hash)
raise PersistedQueryNotFound if query_str.nil?
query_str
end
def store
MemoryStore.instance
end
def ensure_hash(ambiguous_param)
# ...
end
end
Now we need to implement the MemoryStore
(as we know, we need a very simple key-value storage with read and write operations), and that's it, minimalistic back-end support of persisted queries is ready:
class MemoryStore
def self.instance
@instance ||= new
end
def initialize
@storage = {}
end
def fetch_query(hash)
@storage[hash]
end
def save_query(hash, query)
@storage[hash] = query
end
end
And that is it! If you configured your client properly and changed your GraphqlController
, it should work! So you don't have to copy all the boilerplate above between projects, I released a little gem called graphql-ruby-persisted_queries, which implements all the features we discussed earlier, and more:
- Redis storage;
- hash verification;
- hash function configuration;
- Relay support is on its way!
But did not we mention GET requests earlier? Now, we can turn them on, just open up routes.rb
and add the following line:
get "/graphql", to: "graphql#execute"
We also need to slightly change the initialization of apollo-link-persisted-queries
, which should send queries without query string as GET, and full queries as POST:
const link = createPersistedQueryLink({ useGETForHashedQueries: true }).concat(
createHttpLink({ uri: "/graphql" })
);
Persisted queries can help you reduce the amount of traffic between your GraphQL server and its clients by storing query strings at the back-end. If you have the pro version of graphql-ruby
–you're all set, otherwise, take a look at the open-source alternative graphql-ruby-persisted_queries we have just cooked up.
Read more dev articles on https://evilmartians.com/chronicles!
Top comments (0)