You are viewing the preview version of this book
Click here for the full version.

Schema

The schema is the central document defining a GraphQL API. It is a mandatory part, which means you can be sure it is present for every deployment.

The schema defines types and fields. For example, a simple User object with a username and an email can be defined:

type User {
  username: String!
  email: String
}

Fields can reference other types too. For example, Users can belong to a Group:

type Group {
  name: String!
}

type User {
  username: String!
  email: String
  group: Group
}

With just these two things, you can define complex structures that can model to all sorts of use-cases.

Thinking in graphs

The schema defines the structure of the graph of objects. In the above example, Users have a link to a Group, but that only means the client can move from a specific User object to its corresponding Group object.

Apart from types defined in the schema, fields can be scalar too. Some are defined by GraphQL so they are guaranteed to be available in every implementation:

  • String
  • Int
  • Float
  • Boolean
  • ID

ID is a String, it's just not meant to be used for anything other than identifying things.

Query, Mutation, and Subscription

(Official docs)

There are three special types in GraphQL: Query, Mutation, and Subscription.

Query

The Query defines the entry points for a client query. Think about it like REST endpoints that users can call with some parameters and they return some data. The idea here is the same, but instead of returning a fixed structure, a Query provides the first object(s) in the graph.

For example, a query that returns a user by its username:

# schema
type Query {
  user(username: String!): User
}

This gets a username, which is a String (and it's required, as we'll soon see) and returns a User. Then the client can specify what parts of the User it needs by specifying the result fields. And this can span through multiple objects, traversing the graph.

This client query asks for a specific user, its fields, then its group:

# query
query MyQuery {
  user(username: "user1") {
    username
    email
    group {
      name
    }
  }
}

And the result:

{
  "data": {
    "user": {
      "username": "user1",
      "email": null,
      "group": {
        "name": "group 1"
      }
    }
  }
}

Mutation

(Official docs)

Mutations are similar to queries, the difference is only conceptually. They also can get arguments and they can also return objects that clients can use to navigate the graph. But mutations are used to change data instead of query it.

# schema
type Mutation {
  addUser(username: String!, email: String): User
}

And to add the user:

# query
mutation addUser {
  addUser(username: "new user"){
    username
    email
  }
}

This call creates a User object using the arguments provided (username), then return the username and the email fields of the new User.

Query vs Mutation

The only difference between a Query and a Mutation is conceptual:

  • Queries only retrieve data
  • Mutations can change data

But there is nothing that enforces this distinction. We'll see in the Resolvers chapter how to implement what a Query/Mutation does and nothing stops you if you write a Query that modifies data. There are things, such as Subscriptions and caching, that depend on a clear separation of read-only/read-write operations, but otherwise they work the same.

Subscription

Finally, Subscriptions provide a real-time notification channel clients can subscribe to and get events when some data is changed. This is especially useful when you want to build a real-time frontend that automatically fetch new data when it is available. For example, a chat application can work by all participants subscibing to message updates, and whenever a participant wants to sends a message it calls a Mutation.

Real-time notifications
Note

Subscriptions are not supported in all GraphQL implementations. AppSync supports them using a WebSocket channel.

Arguments

(Official docs)

Arguments in the schema can be defined for any fields of any type. In the previous examples we've seen how they work for Queries and Mutations:

type Mutation {
  addUser(username: String!, email: String): User
}

type Query {
  user(username: String!): User
}

But arguments also work for "normal" fields too. Let's say users can have tickets, such as in a ticketing system, and we want to provide a way to get a user's tickets with an optional date filter:

# schema
type Ticket {
  text: String!
  # POSIX time
  created: Int!
}

type User {
  username: String!
  tickets(after: Int): [Ticket]
}

When a nested field needs arguments, the client query can specify that:

# query
query MyQuery {
  user(username: "user1") {
    username
    tickets(after: 1639907490) {
      text
      created
    }
  }
}

When a field does not need arguments in the schema or a client query does not provide them the (...) is missing. The username field in the previous example does not need an argument, but the tickets does. Also, when an argument is optional and the client query chooses not to use that, the parentheses are missing.

Note

How the arguments are used is up to the implementation. GraphQL by default does not handle things like after, limit, offset, or any other arguments.

Lists

(Official docs)

In our examples so far all the fields contained a single value, for example, the User can be part of a single Group. GraphQL supports Lists to define a one-to-many relationship between objects. This is done using the [...] construct.

For example, a Group can contain multiple (0 or more) Users:

# schema
type Group {
  name: String!
  users: [User]
}

type User {
  username: String!
  email: String
  group: Group
}

In a client query there are no differences between a single value (Group) or a List ([User]). The response will contain a JSON array if the field is a List, and an object otherwise.

# query
query MyQuery {
  user(username: "user1") {
    username
    email
    group {
      name
      users {
        username
      }
    }
  }
}

And the response contains all the users in the group:

{
  "data": {
    "user": {
      "username": "user1",
      "email": null,
      "group": {
        "name": "group 1"
        "users": [
          {
            "username": "user1"
          },
          {
            "username": "user2"
          }
        ]
      }
    }
  }
}

Optional vs required fields

GraphQL schema supports marking a field (or argument) as required by adding a ! after it. We've already defined this for the User type:

# schema
type User {
  username: String!
  email: String
}

Here, the username is required, so the response can not be null for this field, while the email is optional. If a client queries both fields, the response is guaranteed to have a non-null value for the username.

In case of Lists, the required indicator can go to two places, each specifying a different requirement:

  • [User!]: The list can not contain nulls
  • [User]!: The list itself can not be null

Here's a table showing what each combination allows:

[User, User][User, null][]null
[User]βœ“βœ“βœ“βœ“
[User!]βœ“-βœ“βœ“
[User]!βœ“βœ“βœ“-
[User!]!βœ“-βœ“-

In practice, most Lists are the strictest variety ([User!]!) as it does not prevent returning empty lists but the caller can be sure it only contains non-null items.

There is more, but you've reached the end of this preview
Read this and all other chapters in full and get lifetime access to:
  • all future updates
  • full web-based access
  • PDF and Epub versions