Under-the-hood of GraphQL

  • Building the schema
  • The query lifecycle
  • Introspection system
  • Libraries
  • Terms
  • Step 1 — Query and schema
  • Step 2 — Execute
  • What have we missed?

1: Overview

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

  1. 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.
  2. A formal language for querying data — what to fetch from where.
  3. Rules for validating or executing a query against the Schema.

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.

const OddType = new GraphQLScalarType({
name: "Odd",
serialize(value) {
if (value % 2 === 1) {
return value
}
},
})

Summary

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

type Book {
id: ID!
title: String
authors: [Author]
}
const Book = new GraphQLObjectType({
name: 'Book',
fields: () => ({
id: { type: new GraphQLNonNull(GraphQLID) },
title: { type: new GraphQLString },
author: { type: new GraphQLList(Author) },
})
}
GraphQLSchema {
astNode: {
kind: 'SchemaDefinition',
...
},
extensionASTNodes: [],
_queryType: Query,
...
_typeMap: {
Query: Query,
ID: ID,
User: User,
String: String,
...
},
...
}

Adding resolvers

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

_typeMap: {
Query: { _fields: { users: { resolve: [function] } } },
User: { _fields: { address: { resolve: [function] } } }
}

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.

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)
{
"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"
}
}
]
}
}
]
}
}
]
}

2. Validate Query

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

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.

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

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.

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.

graphql-js

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.

  • Take output of introspection query and builds the schema out of it
  • Once you have parsed the schema into AST this then transforms into a GraphQLSchema type

graphql-tools

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.

  • Takes arguments:
  • typeDefs - "GraphQL schema language string" or array
  • resolvers - is an object or array of objects
  • Returns a graphql-js GraphQLSchema instance
  • Point this to the source to load your schema from and it returns a GraphQLSchema.
  • 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.

Apollo Studio

Not really a library but I thought worth a mention.

  • Monitoring operations
  • Tracking errors
  • Profiling resolvers (see example on below screen)

2: Building our own GraphQL executor

Here we will build our own GraphQL execute function that a parsed query and our schema can run against. It will ignore any validation. We will use GraphQL instance types to create a schema, ignoring the schema parsing step.

Terms

Definitions:

  • Name for top-level statements in the document
  • Most GraphQL types you define will be object types and they have name and describe fields
  • The type of request i.e query/mutation
  • They are definitions that appear at a single level of a query
  • For example field references e.g a, fragment spreads e.g ...c and inline fragment spreads ...on Type { a }
  • See in the spec
  • Data that must be available at all points during the queries execution.
  • Includes the Schema of the type system currently executing and fragments defined in the query document
  • It is passed into the resolver as context
  • Commonly used to represent an authenticated user or request-specific cache
  • Functions to populate data for a field in your Schema
  • Can return a value, a promise or an array of promises
  1. Step 1 — Query and schema
  2. Step 2 — Execute

Step 1 — Query and schema

Step 1 is to gather our query AST and schema instances using graphql-js, so they are ready for processing

// "document" = is our query in (hardcoded) AST form
const schema = new GraphQLSchema({ …<schema objects> })
// GraphQLSchema contains class instances of root-level query, fields, types, resolvers
ourExecute({ schema, document });
// execute query AST with schema
// schema = built from "schema.graphql" and "graphql-tool" loadSchemaSync/makeExecutableSchema/addResolversToSchema
const document = parse(query)
execute({ schema, document })
  1. The query AST — This is the query put into AST form
  2. The schema — This is how the schema looks (see comment for pre-schema parsing form)

Scenario 1

A root query with resolver args.

Scenario 2

A root query with inner object and resolver args.

Scenario 3

A different root type with inner object.

Step 2 — Execute

Now that we have the setup complete for 3 scenarios we will look to build our actual execute function. The code is below and will be explained.

Checking results

In order to check this would work I wrote some unit tests found here. It includes some pure assertions as well as spies and I initially ran them against graphql-js.execute() to ensure they were written correctly. I then swapped to using my executor. The schema objects are those shown earlier, but this is all of it together.

What have we missed?

As mentioned there are many additional parts to the real graphql executor which we have omitted from our library. Some of those are:

  • Scalar type checks and scalar coercion
  • Processing multiple queries at once
  • Building response data not via pass-by-ref
  • Handling more complex scenarios
  • Validation of query (including errors)
  • Execution context — would be passed into the resolver.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store