How GraphQL turns a query into a response

In this article, I’ll answer a simple question: How does GraphQL turn queries into responses?

If you’re not already familiar with GraphQL, take a look at”How do I GraphQL?” A three-minute introduction to. So you can get more out of this article.

We will introduce the following contents in this article:

  • GraphQL Queries – Queries
  • Schema and resolve functions – Schema and resolve functions
  • GraphQL Execution — step by step

Are you ready? Let’s get started!

GraphQL queries

The GraphQL query structure is very simple and easy to understand. Look at the following example:

{
  subscribers(publication: "apollo-stack"){
    name
    email
  }
}
Copy the code

If we had built an API for Building Apollo, it would be obvious that the query would return the name and email of all subscribers subscribed to “Apollo-stack”. Here’s what the response looks like:

{
  subscribers: [
    { name: "Jane Doe", email: "[email protected]" },
    { name: "John Doe", email: "[email protected]"},... ] }Copy the code

Notice that the structure of the response is almost identical to the structure of the query. The GraphQL client is very simple, and it’s actually self-explanatory!

But what about the server side? Is it more complicated?

As it turns out, the GraphQL server is pretty simple, too. After reading this article, you’ll have a clear idea of what’s going on inside the GraphQL server, and you’ll be ready to build your own.

Schema and Resolve Functions

Each GraphQL server has two core parts that determine how it works: Schema and resolve functions.

Schemas: Schemas are data models that can be retrieved from the GraphQL server. It defines what queries the client is allowed to make, what types of data it can fetch from the server, and the relationships between these types. Such as:

There are three types of simple GraphQL schemas: Author, POST, and Query

In the GraphQL schema syntax, it looks like this:

type Author {
  id: Int
  name: String
  posts: [Post]
}
type Post {
  id: Int
  title: String
  text: String
  author: Author
}
type Query {
  getAuthor(id: Int): Author
  getPostsByTitle(titleContains: String): [Post]
}
schema {
  query: Query
}
Copy the code

Note: In Apollo-server 2.0, the last section of schema can be omitted

This pattern is very simple: it declares that the application has three types: -Author, POST, and Query. Each query must start with one of its fields: getAuthor or getPostsByTitle. You can think of them as REST endpoints, except that they are more powerful.

Author and Post refer to each other. You can get a Post from the posts field of an Author, or you can get an Author from the Post’s Author field.

The schema tells the server what queries clients are allowed to make and the relationships between the different types, but one key piece of information is missing: where each type of data comes from!

That’s what analytic functions are for.

Resolve Functions

Parsing is a bit like routing. They specify how types and fields in a schema are connected to various back ends, addressing the question “How do I get data for Author?” “And” What arguments do I need to call which backend to get the POST data?” Questions like that.

GraphQL parsing functions can contain arbitrary code, which means that the GraphQL server can talk to any type of back end, or even other GraphQL servers. For example, the Author type can be stored in an SQL database, while POST can be stored in MongoDB or even handled by microservices.

Perhaps GraphQL’s greatest feature is that it hides all back-end complexity from the client. No matter how many backends your application uses, the client will just see a GraphQL endpoint with the application’s simple, self-documenting API.

Here are two examples of parsing functions:

getAuthor(_, args){
  return sql.raw('SELECT * FROM authors WHERE id = %s', args.id);
}
posts(author){
  return request(`https://api.blog.io/by_author/${author.id}`);
}
Copy the code

Of course, you don’t write the query or URL directly into a parsing function, but put it in a separate module. But you already know how to use parsing functions.

Query execution — step by step

Ok, now that you know the schema and the parsing functions, let’s take a look at the actual query execution.

Caveat: The code below is for GraphQL-js, which is a JavaScript reference implementation of GraphQL, but the execution model is the same across all GraphQL servers I know of.

At the end of this section, you’ll see how the GraphQL server uses schema and parsing functions to execute queries and generate the desired results.

Here is a query that corresponds to the pattern described earlier. It gets the name of an author, all of that author’s posts, and the name of the author of each post.

{
  getAuthor(id: 5){
    name
    posts {
      title
      author {
        name # this will be the same as the name above}}}}Copy the code

Caveat: If you look closely, you’ll notice that this query gets the same author name twice. I did this here just to illustrate GraphQL, while keeping the schema as simple as possible.

Here are three key steps for the server to respond to a query:

1, parsing,

2, validation,

3, perform

Step 1: Parse the query

First, the server parses the string and converts it into an AST(Abstract syntax tree). If there are any syntax errors, the server stops execution and returns the syntax errors to the client.

Step 2: verify

A query can be grammatically correct and still make no sense, just as The following English sentence is grammatically correct but makes no sense: “The sand left through The idea”.

The validation phase ensures that the given schema query is valid before execution begins. It checks as follows:

  • getAuthorIs it a query type field?
  • getAuthorWhether to accept the nameidThe parameters of the?
  • getAuthorIs there any on the returned typenameandpostsField?
  • . Such as this

As an application developer, you don’t need to worry about this part because the GraphQL server does it automatically. This is in contrast to most restful apis, where it is up to the developer to ensure that all parameters are valid.

Step 3: to execute

If verified, the GraphQL server will execute the query.

Each GraphQL query has the shape of a tree, that is, it is never circular. Execution starts at the root of the Query. First, the executor calls the parsing function for the top-level field – in this case, just getAuthor. It waits until all of these parsing functions return a value, and then continues at the next level in a cascading fashion. If a parse function returns a promise, the performer waits for the promiseresolved.

This is a description of the execution flow. I think things are always easier to understand when they are presented in a different way, so I made a chart, a table, and even a video to take you step by step through it.

Execution flow in graph form:

Execution starts at the top. Parsing functions at the same level are executed concurrently

Table execution flow:

3.1: Run query.getauthor 3.2: Run author.name and author. posts (for Author returned in3) 3.3: Run post.title and post.author (for each Post returned in 3.2)
3.4: run Author.name (for each Author returned in 3.3)
Copy the code

For convenience, this is the same query as above:

{
  getAuthor(id: 5){
    name
    posts {
      title
      author {
        name # this will be the same as the name above}}}}Copy the code

In this query, there is only one root field, getAuthor, and one parameter ID with a value of 5. The getAuthor parse function will execute and return a Promise.

getAuthor(_, { id }){
  return DB.Authors.findOne(id);
}
// let's assume this returns a promise that then resolves to the
// following object from the database: 
{ id: 5.name: "John Doe" }
Copy the code

When the database call returns, the Promise will be resolved. Once that happens, the GraphQL server takes the return value of this parsing function — in this case, an object — and passes it to the parsing function for the name and posts fields on Author, because those are the fields requested in the query. The parsing functions for the name and posts fields are run in parallel.

name(author){
  return author.name;
}
posts(author){
  return DB.Posts.getByAuthorId(author.id);
}
Copy the code

The name parsing function is very simple: it just returns the name attribute of the Author object that was just passed down from the getAuthor parsing function.

The posts parsing function calls the database and returns a list of POST objects:

// list returned by DB.Posts.getByAuthorId(5)
[{
  id: 1,
  title: "Hello World",
  text: "I am here",
  author_id: 5
},{
  id: 2,
  title: "Why am I still up at midnight writing this post?",
  text: "GraphQL's query language is incredibly easy to ...",
  author_id: 5
}]
Copy the code

Note: GraphQL-js waits for all promises in the list to be resolved or Rejected before executing the next level of resolution function

Because the query requests the title and author fields for each post, GraphQL runs four parsing functions in parallel: title and author for each post.

The title function is trivial like name, and the author function is the same as getAuthor’s function, except that it uses the author_id field on POST, while the getAuthor function uses the ID parameter:

author(post){
  return DB.Authors.findOne(post.author_id);
}
Copy the code

Finally, the GraphQL executor calls the Author name resolution function again, this time using the Author object returned by the POSTS Author function. It executes twice — once per post.

At this point the execution is over! All that is left to do is pass the result to the root of the query and return the result:

{
  data: {
    getAuthor: {
      name: "John Doe",
      posts: [
        {
          title: "Hello World",
          author: {
            name: "John Doe"
          }
        },{
          title: "Why am I still up at midnight writing this post?",
          author: {
            name: "John Doe"}}]}}}Copy the code

Note: This example is slightly simplified. A real production GraphQL server would use batch processing and caching to reduce the number of requests to the back end and avoid redundant requests, such as getting the same author twice. But that’s a topic for another article!

conclusion

As you can see, once you dive into it, GraphQL is very easy to understand! I think GraphQL does a great job of solving problems that are difficult to solve in traditional restful apis, such as syntables, filtering, parameter validation, documentation, etc.

Of course, GraphQL is a lot more than WHAT I wrote here, but that’s the subject of a future article!

If this gets you interested in trying GraphQL for yourself, you should check out our GraphQL Server tutorial, or read about using GraphQL on the client together with React + Redux. Related content.

Update 2018: Understand GraphQL execution using Apollo Engine

Since Jonas wrote this article, we’ve also built a service called Apollo Engine that helps developers understand and monitor what’s happening in their GraphQL server by providing the following capabilities:

  • The defect tracking
  • The query cache
  • Field level schema analysis
  • Improved tracking

If you’re interested in seeing your GraphQL queries executed in action, you can log in and inspect your server here. If you are interested in supporting high performance modern applications running with GraphQL, we can help you! Let us know.

Apollo Engine run overview: Query heat maps for service times and request rate/error rate charts.