Vilva Athiban P B JavaScript developer. React, Node, GraphQL. Trying to make the web a better place to browse.

Client-side query customization in GraphQL

4 min read 1299

Client-side Query Customization In GraphQL

The GraphQL world functions with the help of operations like queries, mutations, and subscriptions. Anyone who has worked with GraphQL will know that query structure is generally determined in the backend, and the client is forced to follow it. With GraphQL’s auto-generated docs, this is not a big deal, especially when the developers have access to the backend.

But there can be use cases where the client might have to change the structure of queries or arguments, and this blog will explain one of such use cases and a possible solution for it.

Tools of the trade

Before getting into the details of the problem — and the solution — a basic understanding of the following tools will be helpful.

Hasura

Hasura is an open-source GraphQL service that connects to user databases and microservices and auto-generates a production-ready GraphQL backend. It supports the Postgres database and its flavors (Timescale, Yugabyte). It also allows users to convert their existing database/REST endpoints into a GraphQL database.

graphql-tag

This is a JavaScript template literal tag that parses GraphQL query strings into the standard GraphQL AST. This tool lets us pass a valid query string to the backend, as it handles and auto-fixes a few common issues with query strings. Internally, this tool uses GraphQL’s parser to generate the AST

Background

A team of three frontend and one backend developers started working on a Covid-19 project in our spare time. The backend guy was able to support us only for a very limited time. This put us in a situation where we had to decide on an easy-to-use backend so as not to rely upon the backend dev for too much support.

Hasura to the rescue

That’s when we decided to use Hasura as the backend. With Hasura, creating a schema and data models alone are enough, and it automatically creates the GraphQL backend with the schemas. Another beautiful feature of Hasura is that we can directly query for data from the client using where condition and sort it using order_by condition. This made Hasura our go-to solution.

With all the benefits of Hasura, we were already able to handle everything as a team of frontend developers and didn’t bother the backend developer much. This also helped us prototype the app quickly.

Our tech stack

  • Next.js
  • Apollo
  • Styled-components
  • graphql-tag
  • Hasura
  • Postgres

The problem

Everything was perfect when we started, and it worked great for us. However, since we had to decide how to query the data on the client side, the size of the query string increased drastically, with many nested objects. This messed up the readability of the query, and it was really hard to even check whether the query string was properly structured.

A sample clumsy query will look like this:

We made a custom demo for .
No really. Click here to check it out.

const clumsyHasuraQuery = gql`
  query getProductById($id: Int!) {
    product(
      limit: 10
      offset: 10
      where: { id: { _eq: $id }, quantity: { _gte: 10 }, type_id: { _eq: 10, _gte: 22, _lte: 5, _in: [72,73,74] } }
      order_by: {category: asc, description: desc}
    ) {
      category
      id
    }
  }
`;

The above query shows one of Hasura’s features, with which we can control and get accurate data based on the where condition and sort it using the order_by clause. However, this affected our readability big-time. Since it was a pet project, we visited the codebase once in a while, and we had a hard time understanding the queries.

The solution

We decided to find a solution to fix this readability issue. Our first approach was to dig into existing tooling to find a way out of it. Hasura couldn’t help much in this scenario as a backend. If we wanted to avoid this, we would have had to create custom arguments and make changes to the schema. Thus, we decided to find a solution to the client side.

In the process, we started digging into graphql-tag before we got into Apollo Client. The graphql-tag library was actually parsing the query string to AST. So we decided to write a wrapper over graphql-tag, which gets the query string written in our preferred format, converts it into AST, and transforms it into the structure Hasura would expect on the backend. We named it hql-tag, as it was specific to Hasura.

Step 1: Preferred query structure

We decided to have a straight, single-word argument for where and order_by clauses instead of using nested objects as arguments to the query. Our intended query was supposed to look more elegant, like this:

const elegantHasuraQuery = gql`
  query getProductById($id: Int!) {
   product(
      limit: 10
      offset: 10

      # `${arguementName}_${operatorSuffix}`
      id_eq: $id
      quantity_gte: 10
      type_id_eq: 10
      type_id_gte: 22
      type_id_lte: 5
      type_id_in: [72, 73, 74]
      category_ord: asc
      description_order: desc
    ) {
      category
      id
    }
  }
`;

Step 2: Create hql-tag

Now that we know the structure of both the preferred and the required backend queries, it’s time to create the hql-tag wrapper over graphql-tag.

This wrapper is a JavaScript template literal tag, which receives the query string and passes it down to graphql-tag. Once graphql-tag returns the AST, hql-tag will modify it to suit the needs of the backend.

We considered manipulating the query string directly. but it wasn’t reliable. Therefore, we decided to manipulate the AST. A sample hql-tag implementation:

const hql = (stringArray, ...expressions) => {
  // Generate AST from graphql-tag
  const gqlAst = gql(stringArray, ...expressions);
  // Return the AST if its not query
  if (gqlAst.definitions[0].operation !== "query") return gqlAst;
  // Traverse through AST and modify nodes
  processAST(gqlAst);
  // Return the AST
  return gqlAst;
};

Step 3: Modify AST

We have our AST now, and it’s time to work on the modifications. The process goes like this.

Create AST node templates for the different type of nodes required:

const argumentTemplate = {
  kind: "Argument",
  name: {
    kind: "Name",
    value: "where",
  },
  value: {
    kind: "ObjectValue",
    fields: [],
  },
};

Traverse through the nodes and process the nodes that are arguments:

processNodes(node, parent, key, index);

If it’s an argument, check whether it has any of the predefined operators as a suffix. The format of the customized argument is ${argument_name}_${operator}.

The where arguments should have any one of the supporting arguments, separated by an underscore _. Here are the where operators:

[ "eq", "gte", "gt", "ilike", "in", "like", "lt", "lte", "neq", "nilike", "nin", "nlike", "similar", "nsimilar" ]

The order_by arguments should have a suffix of ord or order. The operators are:

["ord","order"]
1. ord - "short form"
2. order - "full form"

If an argument node has any of the operators, then the argument name and values are extracted and loaded into predefined templates.

- argumentTemplate.value.fields.push(...);

This constructs a new AST node, which replaces the argument node, and the end result is an AST in the format required by the server.

By modifying the AST, we customize the query on the client-side and we were able to achieve a readable and elegant GraphQL query. This hql-tag library is independent and works with all GraphQL client frameworks.

Usage

You can find the hql-tag library on GitHub and npm. Its usage is simple. A graphql-tag query would look like this:

import gql from 'graphql-tag';

const clumsyHasuraQuery = gql`
  //  ... query
`;

The only change required to use the library is to modify the import statement:

import gql from 'hql-tag';

const elegantHasuraQuery = gql`
  //  ... query
`;

Now we are ready to use customized arguments in the queries.

Takeaways and next steps

Though this library was tailor-made for Hasura, the approach of client-side query customization by modifying AST can come in handy when dealing with a large codebase and multiple teams.

This gives us an infinite number of possibilities to write elegant, readable, and reusable code in the GraphQL world. This also adds a vast scope for organizational-level customizations and coding standards on the client side.

The next steps for the library would be to add support for GraphiQL, build a Babel plugin, a plugin for Apollo, add support for all types of AST node templates, modify all node types, and so on.

Monitor failed and slow GraphQL requests in production

While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, try LogRocket.https://logrocket.com/signup/

LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. .
Vilva Athiban P B JavaScript developer. React, Node, GraphQL. Trying to make the web a better place to browse.

Leave a Reply