![]() |
VOOZH | about |
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.
👁 Client-side Query Customization In GraphQLBut 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.
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
Before getting into the details of the problem — and the solution — a basic understanding of the following tools will be helpful.
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-tagThis 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
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.
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.
graphql-tagEverything 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:
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.
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.
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
}
}
`;
hql-tagNow 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;
};
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.
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.
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.
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.
👁 LogRocket Dashboard Free Trial BannerLogRocket lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly aggregating and reporting 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.
Learn how to use Gemini CLI subagents to delegate frontend, backend, testing, and docs tasks to specialized agents with guardrails and clear ownership.
Learn how next-browser gives AI agents runtime context for debugging Next.js apps, including React props, hydration, PPR, forms, and performance.
Build dynamic LLM routing in Next.js with OpenRouter, TanStack AI, task classification, model fallbacks, and cost-aware routing.
TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension — no new framework required.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now