GraphQL basics. Part 2. Schemes and types

GraphQL basics. Part 2. Schemes and types

Each GraphQL service defines a set of types that describe the set of possible data that you can query on that service. This set of types is called a GraphQL schema. Then, when requests come in, they are checked and executed according to this schema.

Type description language

The types are described using the GraphQL schema language. It is similar to the GraphQL query language and allows you to describe schemas without being tied to any language.

Objects and their fields

An object is a special type that can contain fields.

type Character {
  name: String!
  appearsIn: [Episode!]!
}

Character is a GraphQL object. It contains the fields name and appearsIn. String is one of the built-in scalar types. These types cannot have subfields in a query. String! means the field is non-nullable. If as a result of the request it turns out that this field is null, then an error will be returned. [Episode!]! is an array of Episode objects. Since it does not allow null values, you can always expect an array when you request the appearsIn field (with zero or more elements, an empty array is not null). Since Episode! is also non-nullable, you can always expect every element in the array to be an Episode object.

Arguments

Each field of an object can have arguments.

type Starship {
  id: ID!
  name: String!
  length(unit: LengthUnit = METER): Float
}

All arguments are named. Unlike JavaScript, where functions take a list of ordered arguments, all arguments in GraphQL are passed by name. In this example, the length field has one unit argument with a default METER value. If the argument has a default value, it is optional.

The "!" can also be used to test the value of arguments for "null".

query DroidById($id: ID!) {
  droid(id: $id) {
    name
  }
}

In this example, if the id variable is set to null, an error will be returned.

The Query and Mutation Types

There are two special types in the schema: Query and Mutation.

schema {
  query: Query
  mutation: Mutation
}

Every GraphQL service has a query type and can have a mutation type. These types are similar to regular object types, but they are special because they define an entry point for every GraphQL query, for example:

type Query {
  hero(episode: Episode): Character
  droid(id: ID!): Droid
}

The Query type describes all the query options that the client can execute. In the example, we indicate that the client can request fields for hero (type Character) or droid (type Droid). Mutations work in a similar way - you define fields for the Mutation type, and they are available as root mutation fields that you can call in your query.

Scalar types

GraphQL objects have a name and fields, but at some point those fields must be converted to concrete data. This is where scalar types come in handy: they represent the leaves of the query. GraphQL contains several scalar types by default:

  1. Int: 32-bit signed integer.
  2. Float: double-precision signed floating-point number.
  3. String: a sequence of UTF‐8 characters.
  4. Boolean: boolean value true or false.
  5. ID: unique identifier. Serializable in the same way as String. Signals that the field value is not human readable.

You can define your scalar type with the scalar keyword:

scalar Date

Then you must define how to serialize, deserialize, and validate this type. For example, you can specify that the Date type should always be serialized to an integer timestamp, and your client should know that the type is in that format.

Enumerations

An enumeration is a special kind of scalar that is limited to a specific set of valid values. This allows you to check that any arguments of this type are one of the valid values.

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

In this example, the Episode type is limited to three values: NEWHOPE, EMPIRE, and JEDI.

Interfaces

An interface is an abstract type that includes a specific set of fields. For a type to implement an interface, it must include all of its fields. For example, you might have a Character interface that represents any character in the Star Wars trilogy:

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

This means that any type that implements the Character interface must contain all of its fields, with their arguments and return types. For example, here are some types that implement the Character interface:

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}

type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
}

Both of these types contain all the fields from the Character interface, but also include additional fields totalCredits, starships, and primaryFunction that are specific to the character type.

Interfaces are useful when you want to return an object or collection of objects, but they can be of different types. The following request will throw an error:

Query:

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    primaryFunction
  }
}

Variables:

{
  "ep": "JEDI"
}

The hero field returns a Character type, which means it can be either Human or Droid, depending on the argument. In the above query, you can only specify fields that exist in the Character interface, which does not include the Droid type specific primaryFunction field.

To request a specific field for an object type, you need to use the inline fragment:

Query:

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
  }
}

Variables:

{
  "ep": "JEDI"
}

Unions

Unions are very similar to interfaces, but they do not specify common fields for types.

union SearchResult = Human | Droid | Starship

The members of the union type must be specific object types. You can't create a union type from interfaces or other unions.

If the request returns the SearchResult type, then we can get Human, Droid, or Starship. In this case, you need to use inline fragments to be able to query any fields:

{
  search(text: "an") {
    __typename
    ... on Human {
      name
      height
    }
    ... on Droid {
      name
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}

The __typename field is converted to a string that allows different types to be distinguished on the client. Additionally, since Human and Droid types share a common Character interface, you can query their common fields in one place to avoid duplication:

{
  search(text: "an") {
    __typename
    ... on Character {
      name
    }
    ... on Human {
      height
    }
    ... on Droid {
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}

Note that name is still listed in Starship, as it would not otherwise appear in the results as Starship does not implement the Character interface.

Also, by analogy with interfaces, we cannot make a request like this:

{
  search(text: "an") {
    __typename
    name
    ... on Human {
      height
    }
    ... on Droid {
      primaryFunction
    }
    ... on Starship {
      length
    }
  }
}

The error here is that the SearchResult type does not contain a name field. We can only get the result by using inline fragments for the types that are specified in the SearchResult enumeration, or for the interfaces that these types implement.

Input type

GraphQL allows you to pass complex objects as arguments. This is especially useful in the case of mutations, where you want to pass an entire object to add to the database. The input type is defined in the same way as the query type, but using the input keyword.

input ReviewInput {
  stars: Int!
  commentary: String
}

An example of using input type in mutations:

Query:

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

Variables:

{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

The fields in the input object type can themselves refer to the input object types, but you cannot mix the input and output object types in your schema. Input object types also cannot have arguments in their fields.