GraphQL basics. Part 3. Validation and Execution

GraphQL basics. Part 3. Validation and Execution

Validation

The GraphQL type system allows you to validate a query for correctness before executing it. Here are some cases in which an error will be returned:

  1. A field is requested that is not contained in this type.
  2. An object is requested for which the required fields are not specified.
  3. The fields are requested from the scalar.
  4. Fragment refers to itself.
fragment NameAndAppearancesAndFriends on Character {
  name
  appearsIn
  friends {
    ...NameAndAppearancesAndFriends
  }
}

{
  hero {
    ...NameAndAppearancesAndFriends
  }
}

In this example, the fragment refers to itself, which results in an infinite loop.

Execution

Once validated, the GraphQL query is executed by the server, which returns a result (usually in JSON format) that reflects the form of the request. The following types will be used to explain the execution of queries:

type Query {
  human(id: ID!): Human
}

type Human {
  name: String
  appearsIn: [Episode]
  starships: [Starship]
}

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

type Starship {
  name: String
}

Consider the following query:

{
  human(id: 1002) {
    name
    appearsIn
    starships {
      name
    }
  }
}

Each field in a GraphQL query is processed by a corresponding function. This function is called a resolver. If the field is a scalar value such as a string or number, then that value is returned and execution ends. However, if the field is an object, then the corresponding resolver will be called for each of the requested fields of this object.

At the top level of each GraphQL server is a type that represents all possible entry points to the GraphQL API. It is often called Root type or Query type.

Query: {
  human(obj, args, context, info) {
    return context.db.loadHumanByID(args.id).then(
      userData => new Human(userData)
    )
  }
}

In this example, the root type contains a field named human that takes an identifier as an argument. The resolver for this field accesses the database and then creates and returns a Human object. This example is written in JavaScript, however GraphQL servers can be built in many programming languages. Each resolver takes four arguments:

  1. obj The previous object (further discussed in more detail).
  2. args Arguments passed to the field in the GraphQL query.
  3. context Contains contextual information such as the currently logged in user or database access.
  4. info Contains information about the current field and schema.

Asynchronous resolvers

We consider in detail what is happening in this converter:

Query: {
  human(obj, args, context, info) {
    return context.db.loadHumanByID(args.id).then(
      userData => new Human(userData)
    )
  }
}

The context is used to provide access to the database, through which the user data is loaded with a given identifier specified as an argument in a GraphQL query. The resolver returns a Promise because loading from the database is an asynchronous operation. In JavaScript, promises are used to work with asynchronous values, but the same concept exists in many programming languages, often referred to as Futures, Tasks, or Deferred. After receiving data from the database, a new Human object is created and returned. At runtime, GraphQL will wait for Promises, Futures and Tasks to complete before continuing.

Simple resolvers

Now that the Human object is available, the GraphQL server will execute the resolvers for the requested human fields:

Human: {
  name(obj, args, context, info) {
    return obj.name
  }
}

The obj argument is a new Human object returned from the previous human field. In this case, we expect the Human object to have a name property that we return.

In fact, many GraphQL libraries will keep you from writing such simple resolvers and will assume that if there is no resolver for a field, then a property with the same name must be read and returned.

Scalars resolvers

The appearsIn field also has a simple resolver, but let's take a closer look:

Human: {
  appearsIn(obj) {
    return obj.appearsIn // returns [1, 2, 3 ]
  }
}

According to the schema, the appearsIn field is an enumeration with string values, but the function returns numbers. If we look at the result, we can see that the corresponding string enumeration values are returned.

This is an example of scalar conversion. The type system knows what to expect and converts the values returned by the resolver to something that the API supports. In this case, an enumeration must be defined on the server that internally uses numbers like 1, 2, and 3, but converts them to string values in the GraphQL type system.

List resolvers

Human: {
  starships(obj, args, context, info) {
    return obj.starshipIDs.map(
      id => context.db.loadStarshipByID(id).then(
        shipData => new Starship(shipData)
      )
    )
  }
}

In this example, the resolver returns a list of promises. The Human object contains a list of starship identifiers, from which we can obtain starships data. The GraphQL server will execute all of these promises in parallel, wait for all of them to complete, and return a list of Starship objects. Then, a resolver of the name field will be executed in parallel for each of these objects.