GraphQL Server Architecture and Implementation (Part I)

Speaking of GraphQL — one of the first questions is how do you build a GraphQL server? Since GraphQL is only published as a concrete specification, your GraphQL server can really be implemented in any programming language you like.

Before you can start building your server, GraphQL requires you to design a schema that defines the API for your server. In this article, we want to understand the main components of schema and clarify the mechanisms that actually implement it. In this process, we will use libraries such as GraphqL. js, GraphQL-Tools and graphene-js to help understand.

This article only touches on the functionality of native GraphQL — there is no concept of the network layer that defines how servers and clients communicate, and focuses on the inner workings and query process of the “GraphQL execution Engine.” See the next article on the network layer.

GraphQL Schema defines the server API

How to define Schema: Schema Definition Language

GraphQL has its own language that is used to write GraphQL’s Schema: Schema Definition Language (SDL). As a simple example, GraphQL SDL can be used to define the following types:

type User {
  id: ID!
  name: String
}
Copy the code

The User type itself does not expose any functionality to the client application, it simply defines a User model. To add functionality to the API, you need to add fields to the root type of GraphQL Schema (Query, Mutation, and Subscription). These root types define the entry points to the GraphQL API.

For example, look at the following query:

query {
  user(id: "abc") {
    id
    name
  }
}
Copy the code

This Query is valid only if the corresponding GraphQL Schema defines the Query root type and adds the following user field:

typeQuery { user(id: ID!) : User }Copy the code

So, the root type of the schema determines the form of queries and mutations that can be accepted by the server.

GraphQL Schema provides an explicit contract for client-to-server communication.

GraphQLSchemaIs the core object of the GraphQL server

Graphql.js is facebook’s recommended GraphQL implementation, which also provides the basis for other libraries such as GraphQL-Tools and graphene-js. When you use any of these libraries, your development flow revolves around the GraphQLSchema object, which contains the following two main components:

  • The definition of schema
  • A real implementation in the form of a resolver function

For the above example, the GraphQLSchema object would look like this:

const UserType = new GraphQLObjectType({
  name: 'User',
  fields: {
    id: { type: GraphQLID },
    name: { type: GraphQLString },
  },
})

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      user: {
        type: UserType,
        args: {
          id: { type: GraphQLID },
        },
      },
    },
  }),
})
Copy the code

As you can see, the SDL version of schema can be translated directly into the GraphQLSchema type described in Javascript. Note that this schema doesn’t have any resolvers — so it doesn’t allow you to actually perform any queries or variations. More on this in the next section.

The resolver implements the API

GraphQL Server: Structure vs. behavior

GraphQL has a very clear distinction between structure and behavior. The GraphQL server’s structure — as we just discussed — is its Schema, an abstract description of what the server can do. This structure works by identifying a concrete implementation of the server’s behavior. The key component of the implementation is the so-called resolver function.

Each field in GraphQL Schema is supported by a resolver.

In its most basic form, a GraphQL server schema has a resolver function for each field. Each resolver function knows how to get data for that field. Because a GraphQL query is essentially just a collection of fields, all the GraphQL server really needs to do to get the requested data is call the resolver function for the specific fields in the query. (This is why GraphQL is often compared to RPC-style systems, because it is essentially a language that calls remote functions.)

Parse the resolver function

With graphqL.js, each field of any type in the GraphQLSchema object can have a resolve function attached. Let’s consider the above example, specifically the user field on the Query type — we could add a resolve function:

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      user: {
        type: UserType,
        args: {
          id: { type: GraphQLID },
        },
        resolve: (root, args, context, info) => {
          const { id } = args // the `id` argument for this field is declared above
          return fetchUserById(id) // hit the database
        },
      },
    },
  }),
})
Copy the code

Assuming that the fetchUserById function is actually available and returns a User instance (a JS object with id and name fields), it can be said that the resolve function grants the schema the ability to be executed.

Before we go any further, let’s take a moment to understand the four arguments passed to the resolver function:

  1. root(Sometimes also calledparent) : Remember that GraphQL decomposes a query. All you need to do is call the query field decomposer function. It does this by breadth-first (layer by layer) in each resolver callrootThe argument is just the result of the last call (the initial value isnullIf not set separately)
  2. argsThe: argument is used to carry parameters for the query, in this case,UserNeed to takeid.
  3. contextAn object that passes through a chain of resolvers and can be read and written to by each resolver (basically a tool for communicating and sharing information between resolvers).
  4. infoQuery and mutate an AST representation. You can do this in the third article in this series:GraphQL Server Basics: Decrypt the info parameter of the GraphQL resolverFor more details.

We mentioned earlier that every field in GraphQL Schema is supported by a resolver. Now we only have one resolver, and our schema has a total of three fields: the User field on the Query type, plus id and name on the User type. The remaining two fields still need their resolvers. As you can see, implementing these resolvers is trivial:

const UserType = new GraphQLObjectType({
  name: 'User',
  fields: {
    id: {
      type: GraphQLID,
      resolve: (root, args, context, info) => {
        return root.id
      },
    },
    name: {
      type: GraphQLString,
      resolve: (root, args, context, info) => {
        return root.name
      },
    },
  },
})
Copy the code

Query execution

Considering our query above, let’s understand how it is executed to get the data. The query contains three fields: user(root field), ID, and name. This means that when the query reaches the server, the server calls three resolver functions — one for each field. Let’s take a look at the implementation process:

  1. Query arrives at the server
  2. Server is the root fielduserCall the resolver — we assumefetchUserByIdReturn this object:{ "id": "abc", "name": "Sarah" }
  3. The server isUserThe type ofidField calls the resolver. The input argument to the resolverrootThe return value from the last call, so it can simply returnroot.id
  4. Similar to step 3, but returns at the endroot.name. (Note that steps 3 and 4 can be done in parallel)
  5. The decomposition process ends – the final result uses onedataField wrap, which also follows GraphQLThe specific specification
{
  "data": {
    "user": {
      "id": "abc"."name": "Sarah"}}}Copy the code

Optimized request: DataLoader mode

Using the execution method described above, we can easily run into performance problems when clients send deeply nested queries. Imagine our API could also be requested for articles with comments and allow the following queries:

query {
  user(id: "abc") {
    name
    article(title: "GraphQL is great") {
      comments {
        text
        writtenBy {
          name
        }
      }
    }
  }
}
Copy the code

Notice how we request a particular article from a specified user, along with the comments under that article and the names of the users who left those comments.

Let’s say this article has five comments, all written by the same user. This means that we hit writtenBy’s resolver five times, but it returns the same result each time. DataLoader lets you optimize such scenarios to avoid N+1 query problems — the main idea is that the calls to the resolver are batched so that the database (or other data source) is hit only once.

To learn more about DataLoader, you can watch this great video DataLoader – Source Code Walkthrough (about 35 minutes)

GraphQL.js vs graphql-tools

Now, let’s talk about some of the libraries available to help you implement GraphQL server in JavaScript — mainly about the differences between GraphqL.js and GraphQL-Tools.

GraphQL. Js forgraphql-toolsProvides the foundation

The first thing to understand is that graphqL.js provides the foundation for GraphQL-Tools. It does all the heavy lifting by defining the required types, implementing schema construction, and query validation and parsing. Graphql-tools then provides a thin layer of convenience on top of graphqL.js.

Let’s take a quick look at what graphQL.js offers. Note that the functionality revolves around GraphQLSchema:

  • parseandbuildASTSchemaFor the given GraphQL shema (defined as a string in GraphQL SDL), these two functions will create a GraphQLSchema instance:const schema = buildASTSchema(parse(sdlString)).
  • validateGiven a GraphQLSchema instance and a query,validateEnsures that queries adhere to the API defined by this schema.
  • executeGiven a GraphQLSchema instance and a query,executeInvoke the resolver of the query field and create the response according to the GraphQL specification. Of course, this only works if the resolver is part of the GraphQLSchema instance (otherwise, it’s like a restaurant with menus and no kitchen).
  • printSchema: Receives a GraphQLSchema instance and returns its definition in SDL (as a string).

The most important feature in graphqL.js is GraphQL, which takes a GraphQLSchema instance and a query — and then calls validate and execute:

graphql(schema, query).then(result => console.log(result))
Copy the code

To see all of these functions, take a look at this straightforward piece of code.

Here the GraphQL function performs a GraphQL query against a Schema that already contains the structure and behavior. Therefore, the main purpose of GraphQL is to organize the calls to the resolver functions and package the response data based on the content of the supplied query. In this regard, the functionality implemented by the GraphQL function is also called the GraphQL engine.

Graphql-tools: Bridge between interface and implementation

One of the benefits of using GraphQL is that you can follow a schema-first development process, which means that every function you build first is embodied in the GraphQL schema and then implemented by the appropriate resolver. This approach has many benefits, for example, because of THE SDL, it allows front-end developers to start using the emulated APIS before the back-end developers have implemented them.

The biggest drawback of graphqL.js is that it does not allow you to write a schema in SDL and then easily generate an executable version of GraphQLSchema.

As mentioned above, you can create GraphQLSchema instances from SDL using Parse and buildASTSchema, but this lacks the necessary resolve function to make execution possible! The only way to make GraphQLSchema executable (using GraphqL.js) is to manually add the resolve function to the schema’s fields.

Graphql – tools through an important function to fill the gap: addResolveFunctionsToSchema. This is useful because it can be used to provide a better SDL-based API for creating schemas. And that’s exactly what GraphQL-Tools does with makeExecutableSchema:

const { makeExecutableSchema } = require('graphql-tools')

const typeDefs = `
typeQuery { user(id: ID!) : User }type User {
  id: ID!
  name: String
}`

const resolvers = {
  Query: {
    user: (root, args, context, info) => {
      return fetchUserById(args.id)
    },
  },
}

const schema = makeExecutableSchema({
  typeDefs,
  resolvers,
})
Copy the code

So the biggest benefit of using GraphQL-Tools is its nice API for connecting declarative Schemas to resolvers!

When not to use itgraphql-tools

We just learned that graphQL-Tools at its core provides a convenience layer on top of graphqL.js, so in what cases is it not the right choice to implement a server?

Like most abstractions, GraphQL-Tools makes some workflows easier by sacrificing flexibility elsewhere. It provides an excellent “getting started” experience and avoids unnecessary obstacles when building GraphQLSchema quickly. However, if your back end has more customization requirements, such as dynamically constructing and modifying schemas, it might be too restrictive – in which case you can fall back to using GraphQL.js.

aboutgraphene-jsSummary record of

Graphene-js is a new GraphQL library that follows the ideas of its Python equivalents. It also uses graphQL.js behind the scenes, but does not allow schema declarations in SDL.

Graphene-js is deeply integrated into modern JavaScript syntax, providing an intuitive API for implementing queries and variations as JavaScript classes. It’s exciting to see more GraphQL implementations that enrich the ecosystem with novel ideas!

conclusion

In this article, we uncovered the inner workings of the GraphQL execution engine. Starting with the GraphQL Schema used to define the server AIP, you learned that the Schema also determines which queries and variations are accepted, and what format the response should be in. We then delve into the resolver function and summarize the execution mode of the GraphQL engine used to process incoming requests. Finally, we gave an overview of the existing Javascript libraries that help implement the GraphQL server.

In summary, it’s important to know that GraphQL.js provides all the functionality you need to build a GraphQL server — GraphQL-Tools just implements a convenience layer on top of it, satisfies most use cases, and provides an excellent “getting started” experience. Only if you have more advanced requirements for building GraphQL Schema might it make sense to take off the gloves and go straight to GraphQL.js.

In the next article, we’ll discuss the network layer and the different libraries that implement GraphQL server, such as Express-GraphQL, Apollo-Server, and GraphQL-yoga. The third part covers the structure and function of the INFO object in the GraphQL parser.

Click here for the original link