Advanced GraphQL Patterns: Embrace the AST!

by Nicola Marcacci Rossi

May 2019

Apollo Server can be amazingly simple to set up. You start with a simple GraphQL schema, implement a few resolvers, and off you go. But if your use case is more complex, you need a better understanding of what’s going on behind the scenes, and, if possible, how to modify it. This is what we are going to look at in this article.

This article assumes some working knowledge with server-side GraphQL, in particular Apollo Server. It is part of a series on enterprise grade GraphQL hosted by smartive.

Why Explore the Internals of Apollo Server

Perhaps you feel you need to know more about the GraphQL schema:

  • What does Apollo Server do with the GraphQL schema?
  • Where/how can I access it from within a resolver?
  • How can I transform the GraphQL schema?
  • Can I generate the schema (or parts of it) programmatically?

Perhaps what’s puzzling you are the queries received by the GraphQL server:

  • How can I analyze the query received from the client?
  • How can I relate the query to the schema (e.g. to find custom validation directives)?
  • How can I use the query to write more complex resolvers (e.g. recursively joining data in an Almighty Root Resolver)?

To answer these and many more advanced GraphQL questions, you need to become fluent in the internals of Apollo Server. More precisely, you need to understand how Apollo Server employs certain core GraphQL libraries, and how to use them to your advantage.

Abandon Hope All Ye Who Enter Here (Without TypeScript)

Warning: We are entering a scarcely documented area of GraphQL land. You are mostly on your own. TypeScript turned out to be essential for the exploration of these internals. There’s nothing like ctrl-click (other than console.log) to further your insight in these matters.

Understanding GraphQL Schemas

There are a number of ways to define your GraphQL schema. The most straightforward is a schema.graphql file.

graphql
type Book {
title: String
author: String
}
type Query {
books: [Book]
}

You can then define some resolvers and feed everything to Apollo Server.

tsx
import { readFileSync } from "fs";
import { ApolloServer } from "apollo-server";
const typeDefs = readFileSync("schema.graphql", "UTF8");
const resolvers = {
Query: {
books: () => {
/* get them books */
}
}
};
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

OK, but what does Apollo do with that information? Where can I access it? How can I modify it?

Executable Schemas

What Apollo does in the background is:

  1. Parse the schema string into an Abstract Syntax Tree (AST)
  2. Transform the AST into a GraphQLSchema
  3. Add the resolver functions to the GraphQLSchema object

In other words, the above Apollo initialization is equivalent to the following:

tsx
import { buildASTSchema, parse, DocumentNode, GraphQLSchema } from "graphql";
import { addResolveFunctionsToSchema } from "graphql-tools";
const ast: DocumentNode = parse(typeDefs);
const schema: GraphQLSchema = buildASTSchema(ast);
addResolveFunctionsToSchema({
schema,
resolvers
});
const server = new ApolloServer({ schema });

Think about it this way: the AST is a representation of the schema document, while the GraphQLSchema object is a data structure that can resolve GraphQL queries. Note that both these types are defined in the graphql package, the core implementation of the GraphQL language.

Getting the Schema and the AST in a Resolver

So, where do we find the GraphQLSchema object generated during initialization from a resolver? In the mysterious info object.

In the example below, we access the schema and look for a type definition. Note that the types in a GraphQLSchema often don’t contain all needed information. Luckily, the needed AST node is always attached to the equivalent type.

tsx
const resolvers = {
Query: {
books: (parent: any, args: any, ctx: Context, info: GraphQLResolveInfo) => {
const bookType = info.schema.getType("Book") as GraphQLObjectType;
const bookAst: ObjectTypeDefinitionNode = bookType.astNode;
}
}
};

Take a moment to understand the structure of bookAst:

tsx
{
kind: 'ObjectTypeDefinition',
name: {
kind: 'Name',
value: 'Book',
},
interfaces: [],
directives: [],
fields: [
{
kind: 'FieldDefinition',
name: {
kind: 'Name',
value: 'title',
},
arguments: [],
type: {
kind: 'NamedType',
name: {
kind: 'Name',
value: 'String',
},
},
directives: [],
},
{
kind: 'FieldDefinition',
name: {
kind: 'Name',
value: 'author',
},
arguments: [],
type: {
kind: 'NamedType',
name: {
kind: 'Name',
value: 'String',
},
},
directives: [],
}
],
}

As you can see, an Abstract Syntax Tree really is just that: a big plain JavaScript object that represents (part) of the original document.

A rule of thumb: The GraphQLSchema has a few useful utility functions (e.g. to quickly find out inheritance structures), but in general you will find all information you need in AST objects. When an AST object refers to another type you will have to go find it through the GraphQLSchema object.

Harnessing GraphQL Schemas

Knowing these things enables you to use advanced GraphQL programming patterns:

  • Want to programmatically generate a schema? Build a full AST (a DocumentNode object), then feed it to buildASTSchema like in the example above.
  • Want to arbitrarily transform an existing schema? If you have access to the full AST (the DocumentNode object), modify that. I believe the reference to the full AST gets lost though. In that case, use the snippet below to print the schema to string, parse it and modify the resulting AST.
  • Want to use custom schema directives in your resolvers? Inspect the AST.
  • Need to know specific properties of types in your resolvers? Inspect the AST.

The following function is useful if you want to reconvert a schema to string (the original printSchema function in the graphql package loses custom directives).

tsx
import { GraphQLSchema, print } from 'graphql';
export function printSchema(schema: GraphQLSchema): string {
return [
...schema.getDirectives().map(d => print(d.astNode)),
...Object.values(schema.getTypeMap())
.filter(t => !t.name.match(/^__/))
.sort((a, b) => (a.name > b.name ? 1 : -1))
.map(t => print(t.astNode)),
]
.filter(Boolean)
.map(s => `${s}\\n`)
.join('\\n');
}

Understanding GraphQL Queries

Until here we’ve seen: Apollo turns your schema into an object and makes it available in your resolver’s info object.

But what about the queries sent to the server? The same: Apollo turns queries into ASTs and stores them in your resolver’s info object.

Say we send the following query to our GraphQL server:

graphql
query {
books {
title
}
}

Here’s how we find it in our resolver:

tsx
const resolvers = {
Query: {
books: (parent: any, args: any, ctx: Context, info: GraphQLResolveInfo) => {
const fieldName: string = info.fieldName;
const fieldNodes: FieldNode[] = info.fieldNodes;
}
}
};

The value of fieldName is "books", i.e. it lets you access the context of the current resolver. fieldNodes looks like this:

jsx
[
{
kind: 'Field',
name: {
kind: 'Name',
value: 'books',
},
arguments: [],
directives: [],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: {
kind: 'Name',
value: 'title',
},
arguments: [],
directives: [],
},
],
},
},
]

Again, as you see, a plain JavaScript object, this time representing your query.

Note: There are more goodies in the info object. For more info (pun unintended) check out this article.

Harnessing Query Information

With this knowledge, combined with your knowledge of the GraphQLSchema object, you can perform a lot of magic (some of which we will be talking about in future articles), such as:

  • Implementing custom validation decorators
  • Autogenerating SQL Queries
  • Prefetching data needed further down the query in a higher-level resolver (e.g. using SQL JOINS)
  • Implementing an Almighty Root Resolver

Build Enterprise Grade GraphQL Applications

Want to become a GraphQL pro? Follow us and read our whole series on enterprise-grade GraphQL applications.

Need an expert team to implement your advanced web application? Check out our portfolio.

Previous post
Back to overview
Next post