Understanding the GraphQL AST.
The introduction of GraphQL has enabled front-end engineers to craft powerful requests for data without needing to follow a strict back-end response. As Eric Bauer elegantly puts in ‘The Evolution of API design’— GraphQL allows us to ‘express the data as naturally as you can, then work backwards’, thus giving the consumer more control over the data they receive. In order to interpret and understand these unique and complex requests, GraphQL employs the use of an Abstract Syntax Tree (AST) to structure the incoming requests. This makes it easier for the back-end gurus & frameworks to parse and construct the required response.
GraphQL is just a spec and is agnostic of the final data source. Hence, there is a lot of heavy lifting that is performed by libraries such as graphql-js and graphql-tools which abstracts away the need to directly interact with the underlying AST. However, it is often the case that we need to perform our own custom operations based on the specifics of the user’s request.
So what is an Abstract Syntax Tree?
As Stephen Schneider puts in ‘The GraphQL AST— Dawn of a schema’, an AST is just a ‘fancy way of saying heavily nested objects’. It is a structure that is commonly used in compilers to parse the code we write, converting it into a tree structure that we can traverse programmatically.
When a user makes a request, GraphQL combines the query document that the user requested with the schema definition that we defined for our resolver in the form of an AST. This AST is used to determine which fields were requested, what arguments were included and much more.
Imagine that you want to expose a list of rugby players on your GraphQL backend. You’re schema definition might look like the following.
When a consumer makes a request for a list of users, the GraphQL schema has no knowledge of the query document (request). It therefore parses the query document into the AST format and traverses it to perform any necessary validation such as throwing errors against incorrect fields or arguments. Once validation has occurred, schema types are mapped onto the respected ‘branches’ on the AST to provide even more useful metadata.
Suppose that a consumer wanted to fetch a list of rugby players and the information of what team they played for. The query document would look like the following:
parent— Otherwise known as the root — since resolvers can delegate to other resolvers this argument contains the result of the previous call
args— parameters requested by the user for the query
context— ‘global’ variable passed through the resolver chain. Useful for communicating between different layers
info— This is our crown jewel. This is our AST
The generated AST from the query document would look like the following:
Some of the important fields include
fieldName: the name of the current resolver
fieldNodes: an array of fields left in the current selectionSet (group of fields on current object we are traversing in the tree)
path:Keeps track of all the parent fields that led to the current branch
operation:The entire query info. (this is a global field that will be the same for any resolver. Other global fields include
The tree structure of an AST naturally fits the way we make GraphQL requests with nested objects. The above AST neatly identifies our root
full_name field and also our nested
club field. We can traverse the
selectionSet on club to reveal the properties that a user requested. Tools such as graphql-tools make it easier to traverse these selection sets and interact with the AST.
So why is all this important?
When I first starting implementing a GraphQL backend, my thoughts were not “I can’t wait until I can dive into that confusing GraphQL AST that I keep seeing scattered throughout my resolvers”.
Instead it arose out of need to craft custom directives and optimise user requests. It has been very useful to break the traditional GraphQL lifecycle and intercept a request before it gets passed onto another library to generate a data response. Specifically, by traversing and augmenting the AST we can implement:
- Schema stitching
- Custom directives
- Enriched queries
- Layered Abstraction
- More backend magic!
In a step to give more control to the front-end client, we often find it useful to implement a range of directives that can transform or filter out fields specified in the AST.
It is often very common that rugby commentators mis-pronounce the players name. To help our faithful commentators, we have decided to implement a rugby player directive to help with pronunciations.
To achieve the above, we can create a
@pronounce directive that will give us the phonetic spelling of players names.
A simple implementation of a magic service can then help us ‘translate’ these players names by diving into the
directives array on the
full_name selection in the generated AST.
Our resolve function now dives into the AST to find which fields are tagged with the
@pronounce directive and translates the fields accordingly.
With a few lines of code we have solved the issue of dodgy name pronunciations.
What nightmares are made of.
Many objects in a GraphQL data source do not change that often. It becomes quite expensive to fetch these items every time a user requests it and since users define the shape of a GraphQL response, it can be quite difficult to cache and return a custom response for each user.
A common solution to this is caching a result against a unique stringified version of the AST field selections. By combining the requested fields and their nested fields, we can create a key that will match for all users with the same query document. This enables us to reuse a result instead of fetching from a database or external data source once again.
Understanding how the AST is structured and used within your resolvers allows you to gain a fine-grain control over the input and output of your GraphQL backend. It allows you to cache results, create custom directives, optimise queries & stitch together data from multiple resources.
By understanding how the AST works in the GraphQL lifecycle, you can create complex backends and ultimately provide more power to the front end consumer.