Persisted queries in GraphQL: Slim down Apollo requests to your Ruby application

Learn how to reduce the size of network requests from the Apollo client in the frontend to the GraphQL Ruby backend 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: backend describes the data model using types, so frontend 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?

First of all, the amount and the variety of often-used queries in your application are limited: usually, frontend knows precisely what it needs from the backend for every particular view. Popular GraphQL frameworks like Apollo and Relay use that fact to persist queries in the backend so that a frontend 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.

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 backend 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: backend 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 backend. 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 backend will persist it to the store.

Now we need to implement our backend 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 backend 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 backend. 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.

Join our email newsletter

Get all the new posts delivered directly to your inbox. Unsubscribe anytime.