Setting Up Automatic Persisted Queries in GraphQL for Gloo Gateway

GraphQL is a server-side query language and runtime you can use to expose your APIs as an alternative to REST APIs. GraphQL allows you to request only the data you want and handle any subsequent requests on the server side, saving numerous, expensive origin-to-client requests by instead handling requests in your internal network.

Stitching together your APIs into a single GraphQL schema offers many benefits. However, the resulting schema can potentially be very large. In that case the query strings that a client needs to send to the GraphQL server also grows, impacting network latency and performance.

To overcome this challenge, GraphQL supports Automatic Persisted Queries (APQ). APQ allows the client to send a query and its SHA256 hash to the GraphQL server. Then, subsequent requests can execute the same query simply by sending the hash. This way, APQ removes the need to send large query strings over the wire. It caches the request not only on the GraphQL server side, but also in intermediate Content Delivery Networks (CDNs).

GraphQL for Gloo Gateway provides first-class support for APQ through its policy framework. Gloo’s declarative API simplifies setup and configuration, especially in a GitOps controlled environment.

In this blog we will walk you through an example of how to set up APQ in a GraphQL for Gloo Gateway environment. You’ll learn how to send a GraphQL request to the GraphQL for Gloo Gateway server that is cached for subsequent calls.

Enabling APQ

For this example, we will use the Istio Bookstore application with a GraphQL API. The following snippet shows the GraphQL Schema Definition Language (SDL) of the Bookinfo service. For more information about deploying GraphQL services in Gloo, please refer to the product documentation.

type Query {
  """Description of a book in HTML"""
  productsForHome: [Product] 
}

"""Each book has a product entry"""
type Product {
  """Unique identifier for books"""
  id: String
  """The book title"""
  title: String
  """Description of a book in HTML"""
  descriptionHtml: String
  """List of reader reviews for this book. Queries the reviews REST service"""
  reviews: [Review] 
  """List of reader ratings for this book. Queries the ratings REST service"""
  ratings: [Rating] 
}

Our GraphQL service is deployed through a Gloo RouteTable. The following is a snippet of the RouteTable that defines the route to our GraphQL service:

http:
  - name: bookinfo-graphql
    labels:
      route: bookinfo-graphql
    matchers:
    - uri:
        prefix: /bookinfo-graphql
    graphql:
      schema:
        name: bookinfo-graphql-schema
        namespace: bookinfo
        clusterName: gg-demo-single

To enable Automatic Persisted Queries, we use a GraphQLPersistedQueryCachePolicy. We configure this policy to apply to all routes with the label “route: bookinfo-graphql“, such as in the previous route configuration. We also configure the cache size of our query cache, such as in the following example:

apiVersion: resilience.policy.gloo.solo.io/v2
kind: GraphQLPersistedQueryCachePolicy
metadata:
  name: bookinfo-graphql-query-cache
  namespace: bookinfo
spec:
  applyToRoutes:
  - route:
      labels:
        route: bookinfo-graphql
  config:
    cacheSize: 1000

Using Persisted Queries

With the policy applied to the route of our GraphQL service, we can test persisted queries. In this example, we will use a simple query that returns the title of our book:

query ProductsForHome {
  productsForHome {
    title
  }
}

To send the query, you typically use a GraphQL client. However, most GraphQL clients have native support for persisted queries. This means that steps such as calculating the query hash and registering the query are done automatically for you. Instead, we will send the query by using cURL so that we can understand how these steps work with persisted queries.

To call a persisted query, instead of sending the query string to the GraphQL server, we will send the SHA-256 hash of our query. If the query that matches that hash is cached on the GraphQL server, the query will be executed and the result returned to the client. If the query with that hash has not yet been cached, an error will be returned.

First, let’s store our GraphQL query string in an environment variable to make it easier to send our queries:

$ export QUERY="query MyProductsForHome { productsForHome { title } }"

Next, we need to compute the SHA-256 hash of this particular query. We will use the shasum command for this. Note that shasum not only prints the hash, but also the name of the input file. Because we know that the SHA-256 hash is always 64 characters long, we can grab the first 64 characters of the output using the head command. We store the hash in another environment variable:

$ export QUERY_SHA256=$(echo -n $QUERY | shasum -a 256 | head -c 64)

The calculated hash looks like this:

$ echo $QUERY_SHA256

ba7faf706579b441e281376ba5a87d5047e79eb52dcf6ac0eb34eb85ed53b053

Now, we can call our GraphQL service. We will first send a GET request with the SHA-256 of our query, to try to call a cached version of our query. Because our query has not yet been cached by the GraphQL server, the server returns an error:

$ curl --get http://graphql.api.example.com/bookinfo-graphql \
    --data-urlencode "extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"$QUERY_SHA256\"}}"

{"errors":[{"message":"persisted query not found: sha256 ba7faf706579b441e281376ba5a87d5047e79eb52dcf6ac0eb34eb85ed53b053"}]}

This tells our client that it needs to send the query again, this time with the query string.

To send our GraphQL query string, we first need to encode it to a URL-encoded format. We can use a simple Python script to encode and store the result in another environment variable:

$ export QUERY_ENCODED=$(python -c "import urllib, sys; print urllib.quote(sys.argv[1])" "$QUERY")

Our URL-encoded query looks like this:

$ echo $QUERY_ENCODED

query%20MyProductsForHome%20%7B%20productsForHome%20%7B%20title%20%7D%20%7D

Next, we can call our service with the query string and hash, and get the query response from our GraphQL server:

$ curl --get http://graphql.api.example.com/bookinfo-graphql \
    -d "query=$QUERY_ENCODED" \
    --data-urlencode "extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"$QUERY_SHA256\"}}"

{"data":{"productsForHome":[{"title":"The Comedy of Errors"}]}}

Now, the query is also cached on the GraphQL server side. To execute the query again, we no longer have to provide the query string. Instead, we can send only the hash, and the server will execute the cached query and return the query result:

$ curl --get http://graphql.api.example.com/bookinfo-graphql \
    -d "query=$QUERY_ENCODED" \
    --data-urlencode "extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"$QUERY_SHA256\"}}"

{"data":{"productsForHome":[{"title":"The Comedy of Errors"}]}}

In this example, we used a simple small query, but imagine a query string with dozens or even hundreds of fields. By sending just the hash of the query, you can save processing power on the client and server sides, as well as network bandwidth. APQ improves both the latency and performance of your queries.

Furthermore, by using an HTTP GET request, you can cache the query on a CDN or any HTTP proxy. GraphQL for Gloo Gateway provides many options to configure HTTP cache-control directives, such as “max-age.” This gives you more control over caching GraphQL queries in a CDN. For more information, please consult the GraphQL for Gloo Gateway documentation.

Conclusion

GraphQL for Gloo Gateway is a powerful GraphQL server implemented directly in the Envoy-based Gloo Gateway proxy. The ability to define GraphQL APIs and configurations in cloud-native declarative fashion using Kubernetes based APIs makes the platform extremely suitable for GitOps based environments.

Automatic Persisted Queries is one of the GraphQL for Gloo Gateway features that enables users to send even the largest and most complex queries in low-latency and high performance environments.

We’ve shown both how to enable the query caching feature of GraphQL for Gloo Gateway using declarative policies and how to register and call persisted queries from a GraphQL client.

Learn more about GraphQL for Gloo Gateway here.