We recently had the opportunity to work on a GraphQL project together. Before starting the project, we sat down to check our understanding of how queries were executed. As visual learners, we searched for a diagram that would explain the request lifecycle, but couldn’t find one that met our needs. We decided to embark on a journey and map out a diagram along the way:
For our trip, we started with a database of cities
. An adventurer
can
search cities
based on language
and then look at the corresponding reviews
from fellow adventurers
. A review
for a city
contains the adventurer
who gave the review
, a numerical rating
out of 5, and an optional comment
.
This is what that GraphQL request would look like for cities where the language “Arturan” is spoken:
query {
cities(language: "Aturan") {
id
name
language
reviews {
adventurer {
name
}
rating
comment
}
}
}
The response will be in JSON and look like this:
{
"data": {
"cities": [
{
"id": "1",
"language": "Aturan",
"name": "Imre",
"reviews": [
{
"adventurer": {
"name": "Kvothe"
},
"rating": 5,
"comment": "Check out The Eolian for some good music"
}
...
]
}
...
]
}
}
The official GraphQL specification ensures that the response will have a
top-level data
key, but otherwise we can see that our response follows the
shape of the request very closely. The ability to quickly and flexibly request
data is part of the appeal of GraphQL.
So what was the path we took from request to response?
1. The client sends a POST request with the query in the body.
Since this is a web application, we are using HTTP for our client-server protocol, although GraphQL is not limited to any specific protocol. Typically, all GraphQL requests are POST method requests, whether they are requesting information (a “query” in GraphQL parlance), or changing data (a “mutation”). A GET request is possible using query params, but this is not as common.
When we initially execute the query, the client sends a POST request to a single route, usually “/graphql”. In order to simplify managing the request on the client side, it is common to use a GraphQL client, such as Apollo or Relay. A GraphQL client is a layer between the server and the web client that offers additional functionality, such as integrations with frontend frameworks and intelligently caching/managing responses.
2. The server parses and validates the request.
After the client issues the request, the server will parse the body, generating an Abstract Syntax Tree (AST) based on the query. ASTs are tree data structures commonly used to present important syntactic elements of bits of code. A GraphQL AST will encode information about the query, the types and values involved, and lots of other pieces of information that will allow the server to validate and work with the request.
An excerpt of a GraphQL AST might look something like this:
{
"kind": "Document",
"definitions": [
{
"kind": "OperationDefinition",
"operation": "query",
"variableDefinitions": [],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "cities",
"loc": {
"start": 139,
"end": 145
}
} ...
After generating the AST, the server validates the request based on the schema you’ve defined. This validation check happens automatically for you, so there’s no need to worry about writing additional code to check for invalid params.
The reason that GraphQL can do this is because it uses something called a Schema Definition Language, or SDL, to define types and the relationships between the types. In the example query above, we are requesting data for adventurers, cities, and reviews.
Here are what our schema would look like in the GraphQL SDL:
type City {
id: Int!
language: String!
name: String!
reviews: [Review!]
}
type Review {
id: Int!
rating: Int!
comment: String
adventurer: Adventurer!
}
type Adventurer {
id: Int!
name: String!
}
GraphQL clients like Apollo can generate type files for you for many different languages, including Scala, Swift, and TypeScript. To build these type files, Apollo will use an introspection query. At a high level, introspection queries are API queries that return data about the API itself.
If you are working with an API you have never seen before, you can use an introspection query to request information from the API about the allowed queries and data types. A popular tool called GraphiQL, an in-browser IDE for inspecting GraphQL APIs, uses an introspection query to provide realtime documentation and type checking.
3. The server executes the query.
Once the query has been validated, the server will do the work of fetching and composing the requested information. One by one, resolver functions for each of our query fields will get executed.
The individual resolvers also handle any arguments passed to the query, such as filtering or pagination params. These arguments are handled similarly to fields in the query, meaning they are type checked and executed within the appropriate resolver.
Our root level query is cities
, so we will initially execute the resolver for
cities, and then move downward, executing resolvers for each field in the schema
definition. The execution will wait until all the resolvers are finished, at
which point the data will be returned in the response.
As you might imagine, we could easily run into situations where we are querying duplicate data, like if we fetched the same adventurer from the database multiple times. There are many strategies to avoid making duplicate requests, including caching and batching. Additionally, in order to avoid infinite loops or overly complex queries (like ones that are deeply nested or excessively large), depth limits and field constraints can be set for each query.
4. The client receives the response and updates client-side state.
At this point, our request has been validated and resolved by the server, and only the information the client asked for, with no extraneous fields or associations, is returned.
This is one of the major benefits of GraphQL: data management on the front-end can be moved to the component level. Each front-end component can decide what data it needs locally, create the appropriate query snippet, pass that to a GraphQL client, and then manage its data and state locally. Pushing down the data management to the component level has replaced application state management libraries like Redux.