Under-the-hood of GraphQL
By: Craig Taub
Tags: GraphQL, JavaScript, Nodejs, Apollo Server, Type Systems

1: Overview

First we need to ask what is graphql? There are a couple of answers

It is:

A type system — type definitions define how data should look. They are written to show what is included and highlight the relationships (how things relate). This system definition language can be transformed into AST. A formal language for querying data — what to fetch from where. Rules for validating or executing a query against the Schema.

Point (3) above references the official Specification which defines the rules for types, validation and executing the schema. It can be found at:

There is also a documentation-friendly website graphql.org/learn

All languages follow the spec, and the JS graphql library frequently references part of it. We will be looking at that library as part of this section.

Building the schema

The schema is an important part of a graphql application, as mentioned above it defines all types and their relationships. There are 2 steps to this

1. Parses “schema notation” (usually found in a schema.graphql file) into AST

The parser will throw errors if it is not a GraphQL schema. See snippet from types/schema.js (below)

export function isSchema(schema) {

return instanceOf(schema, GraphQLSchema)

}

2. Transform AST into objects and instances

We need a schema which is a type instance of GraphQLSchema (see above snippet). We then need objects inside the schema which match types, for example a scalar or an object.

Example of a scalar (below snippet)

const OddType = new GraphQLScalarType({

name: "Odd",

serialize(value) {

if (value % 2 === 1) {

return value

}

},

})

The native scalar types defined for GraphQL are ID , Int , Float , String and Boolean . You can define your own inside your type system.

Summary

Essentially for building the schema we turn this graphql schema notation:

type Book {

id: ID!

title: String

authors: [Author]

}

Into this Javascript

const Book = new GraphQLObjectType({

name: 'Book',

fields: () => ({

id: { type: new GraphQLNonNull(GraphQLID) },

title: { type: new GraphQLString },

author: { type: new GraphQLList(Author) },

})

}

Almost all of the GraphQL types that you define will be object types. Object types have a name, but most importantly describe their fields.

A GraphQLSchema looks like the below in raw POJO form:

GraphQLSchema {

astNode: {

kind: 'SchemaDefinition',

...

},

extensionASTNodes: [],

_queryType: Query,

...

_typeMap: {

Query: Query,

ID: ID,

User: User,

String: String,

...

},

...

}

It holds AST information too but the root Query and types are found under the _typeMap property.

Adding resolvers

Resolvers cannot be included in the GraphQL schema language, so they must be added separately.

They are added to the _typeMap property, under _typeMap.<Type>._fields.<field> So after adding the resolvers, a schema object might look like below:

e.g.

_typeMap: {

Query: { _fields: { users: { resolve: [function] } } },

User: { _fields: { address: { resolve: [function] } } }

}

From the HTTP request side the community has largely standardized on a HTTP POST method. But what happens when the server receives a query?

Query lifecycle

The GraphQL spec outlines what is known as the “request lifecycle”. This details what happens when a request reaches the server to produce the result.

There are 3 steps that occur once the lifecycle is triggered.

1. Parse Query

Here the server turns the query into AST. This includes:

Lexical Analysis -> GraphQL’s Lexer identifies the pieces (words/tokens) of the GraphQL query and assigns meaning to each

Syntactic Analysis -> GraphQL’s parser than checks whether the pieces conforms to the language syntax (grammar rules)

If both these pass the server can move on.

In graphql-js this is all found under the parser.js file and function.

This query:

query homepage { posts { title author } }

Becomes the AST

{

"kind": "Document",

"definitions": [

{

"kind": "OperationDefinition",

"operation": "query",

"name": {

"kind": "Name",

"value": "homepage"

},

"selectionSet": {

"kind": "SelectionSet",

"selections": [

{

"kind": "Field",

"name": {

"kind": "Name",

"value": "posts"

},

"selectionSet": {

"kind": "SelectionSet",

"selections": [

{

"kind": "Field",

"name": {

"kind": "Name",

"value": "title"

}

},

{

"kind": "Field",

"name": {

"kind": "Name",

"value": "author"

}

}

]

}

}

]

}

}

]

}

You can see this for yourself on astexplorer under GraphQL.

2. Validate Query

This step ensures the request is executable against the provided Schema. Found under the validate.js in the graphql-js library.

While it is usally run just before execute, it can be useful to run in isolation. For example by a client before sending the query to the server. The benefit is that the validator could flag an invalid query before it is sent to the server, saving a HTTP request.

It works by checking each field in the query AST document against its corresponding type definition in the schema object. It will compare argument type compatibility as well as coercion checks.

3. Execute Query

This step is by far the most intensive and the step I often found the most confusing in its mechanism. We will be digging deeper into this step in part 2, so lets look at the process involved from a high-level.

Identify the operations i.e. a query or mutation? (several queries can be made at once) Then resolve each operation.

For step (2) each query/mutation is run in isolation.

Resolving each operation

For step 2 GraphQL iterates over each field in the selection-set, if it is a scalar type resolve the field ( executeField ) else recurse the selection-sets until it resolves to a scalar.

The way this works is that the engine calls all fields on the root level at once and waits for all to return (this includes any promises). Then after reading the return type it cascades down the tree calling all sub-field resolvers with data from the parent resolver. Then it repeats this cascade on those field return types. So simply speaking it calls the top-level Query initially and then the root resolver for each type.

The mechanism caled “scalar coercion” comes into play here. Any values returned by the resolver are converted (based on the return type) in order to uphold the API contract. For example a resolver returning string “123” with a number type attached to it will return Number("123") (i.e a number).

This is found under the execute.js file and function inside graphql-js .

Lastly the result is returned.

Introspection system

Its worth mentioning the introspection system. This is a mechanism used by the GraphQL API schema to allow clients to learn what types and operations are supported and in what format.

The clients can query the __schema field in the GraphQL API, which is always available on the root type of a Query. Any interactive GraphQL UIs rely on sending an IntrospectionQuery request to the server, which builds documentation and auto-completion with it.

Libraries

As part of my research I covered many different libraries, so I thought it was worth giving a quick overview of the main ones in the JS ecosystem.

The reference implementation of the GraphQL spec, but also full of useful tools for building GraphQL servers, clients and tooling. It’s a GitHub organisation with many mono-repositories.

It performs the entire Query Lifecycle (including parsing schema notation).

Schema

Requires library specific GraphQLSchema instance. In order to be an executable schema it requires resolver functions.

Example functions

buildClientSchema

Take output of introspection query and builds the schema out of it

buildASTSchema

Once you have parsed the schema into AST this then transforms into a GraphQLSchema type

It’s an abstraction on top of graphql-js . Houses lots of functionality including generating a fully spec-supported schema and stitching multiple schemas together.

Example functions

makeExecutableSchema

Takes arguments:

typeDefs - "GraphQL schema language string" or array

- "GraphQL schema language string" or array resolvers - is an object or array of objects

- is an object or array of objects Returns a graphql-js GraphQLSchema instance

loadSchemaSync

Point this to the source to load your schema from and it returns a GraphQLSchema .

addResolversToSchema

Takes a GraphQLSchema and resolvers then returns an updated GraphQLSchema .

apollo-server

It’s also an abstraction on graphql-js . Uses the graphql-tools library for building GraphQL servers.

Introspection is disabled on production by default but on non-production it uses introspection to expose a /graphql playground.

Apollo Studio

Not really a library but I thought worth a mention.

Plugs into apollo-server and provides stats and information of your GraphQL server in realtime. Such as: