Martian Chronicles
Evil Martians’ team blog
Back-end

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 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.

Humans! We come in peace and bring cookies. We also care about your privacy: if you want to know more or withdraw your consent, please see the Privacy Policy.