Chapter 4. Designing a Schema

GraphQL is going to change your design process. Instead of looking at your APIs as a collection of REST endpoints, you are going to begin looking at your APIs as collections of types. Before breaking ground on your new API, you need to think about, talk about, and formally define the data types that your API will expose. This collection of types is called a schema.

Schema First is a design methodology that will get all of your teams on the same page about the data types that make up your application. The backend team will have a clear understanding about the data that it needs to store and deliver. The frontend team will have the definitions that it needs to begin building user interfaces. Everyone will have a clear vocabulary that they can use to communicate about the system they are building. In short, everyone can get to work.

To facilitate defining types, GraphQL comes with a language that we can use to define our schemas, called the Schema Definition Language, or SDL. Just like the GraphQL Query Language, the GraphQL SDL is the same no matter what language or framework you use to construct your applications. GraphQL schema documents are text documents that define the types available in an application, and they are later used by both clients and servers to validate GraphQL requests.

In this chapter, we take a look at the GraphQL SDL and build a schema for a photo sharing application.

Defining Types

The best way to learn about GraphQL types and schemas is to build one. The photo sharing application will let users log in with their GitHub accounts to post photos and tag users in those photos. Managing users and posts represents functionality that is core to just about every type of internet application.

The PhotoShare application will have two main types: User and Photo. Let’s get started designing the schema for the entire application.

Types

The core unit of any GraphQL Schema is the type. In GraphQL, a type represents a custom object and these objects describe your application’s core features. For example, a social media application consists of Users and Posts. A blog would consist of Categories and Articles. The types represent your application’s data.

If you were to build Twitter from scratch, a Post would contain the text that the user wishes to broadcast. (In this case, a Tweet might be a better name for that type.) If you were building Snapchat, a Post would contain an image and would more appropriately be named a Snap. When defining a schema, you will define a common language that your team will use when talking about your domain objects.

A type has fields that represent the data associated with each object. Each field returns a specific type of data. This could mean an integer or a string, but it also could mean a custom object type or list of types.

A schema is a collection of type definitions. You can write your schemas in a JavaScript file as a string or in any text file. These files usually carry the .graphql extension.

Let’s define the first GraphQL object type in our schema file—the Photo:

type Photo {
    id: ID!
    name: String!
    url: String!
    description: String
}

Between the curly brackets, we’ve defined the Photo’s fields. The Photo’s url is a reference to the location of the image file. This description also contains some metadata about the Photo: a name and a description. Finally, each Photo will have an ID, a unique identifier that can be used as a key to access the photo.

Each field contains data of a specific type. We have defined only one custom type in our schema, the Photo, but GraphQL comes with some built-in types that we can use for our fields. These built-in types are called scalar types. The description, name, and url fields use the String scalar type. The data that is returned when we query these fields will be JSON strings. The exclamation point specifies that the field is non-nullable, which means that the name and url fields must return some data in each query. The description is nullable, which means that photo descriptions are optional. When queried, this field could return null.

The Photo’s ID field specifies a unique identifier for each photo. In GraphQL, the ID scalar type is used when a unique identifier should be returned. The JSON value for this identifier will be a string, but this string will be validated as a unique value.

Scalar Types

GraphQL’s built in scalar types (Int, Float, String, Boolean, ID) are very useful, but there might be times when you want to define your own custom scalar types. A scalar type is not an object type. It does not have fields. However, when implementing a GraphQL service, you can specify how custom scalar types should be validated; for example:

scalar DateTime

type Photo {
    id: ID!
    name: String!
    url: String!
    description: String
    created: DateTime!
}

Here, we have created a custom scalar type: DateTime. Now we can find out when each photo was created. Any field marked DateTime will return a JSON string, but we can use the custom scalar to make sure that string can be serialized, validated, and formatted as an official date and time.

You can declare custom scalars for any type that you need to validate.

Note

The graphql-custom-types npm package contains some commonly used custom scalar types that you can quickly add to your Node.js GraphQL service.

Enums

Enumeration types, or enums, are scalar types that allow a field to return a restrictive set of string values. When you want to make sure that a field returns one value from a limited set of values, you can use an enum type.

For example, let’s create an enum type called PhotoCategory that defines the type of photo that is being posted from a set of five possible choices: SELFIE, PORTRAIT, ACTION, LANDSCAPE, or GRAPHIC:

enum PhotoCategory {
    SELFIE
    PORTRAIT
    ACTION
    LANDSCAPE
    GRAPHIC
}

You can use enumeration types when defining fields. Let’s add a category field to our Photo object type:

type Photo {
    id: ID!
    name: String!
    url: String!
    description: String
    created: DateTime!
    category: PhotoCategory!
}

Now that we have added category, we will make sure that it returns one of the five valid values when we implement the service.

Note

It does not matter whether your implementation has full support for enumeration types. You can implement GraphQL enumeration fields in any language.

Connections and Lists

When you create GraphQL schemas, you can define fields that return lists of any GraphQL type. Lists are created by surrounding a GraphQL type with square brackets. [String] defines a list of strings and [PhotoCategory] defines a list of photo categories. As Chapter 3 discusses, lists can also consist of multiple types if we incorporate union or interface types. We discuss these types of lists in greater detail toward the end of this chapter.

Sometimes, the exclamation point can be a little tricky when defining lists. When the exclamation point comes after the closing square bracket, it means that the field itself is non-nullable. When the exclamation point comes before the closing square bracket, it means that the values contained in the list are non-nullable. Wherever you see an exclamation point, the value is required and cannot return null. Table 4-1 defines these various situations.

Table 4-1. Nullability rules with lists
list declaration definition

[Int]

A list of nullable integer values

[Int!]

A list of non-nullable integer values

[Int]!

A non-nullable list of nullable integer values

[Int!]!

A non-nullable list of non-nullable integer values

Most list definitions are non-nullable lists of non-nullable values. This is because we typically do not want values within our list to be null. We should filter out any null values ahead of time. If our list doesn’t contain any values, we can simply return an empty JSON array; for example, []. An empty array is technically not null: it is just an array that doesn’t contain any values.

The ability to connect data and query multiple types of related data is a very important feature. When we create lists of our custom object types, we are using this powerful feature and connecting objects to one another.

In this section, we cover how to use a list to connect object types.

One-to-One Connections

When we create fields based on custom object types, we are connecting two objects. In graph theory, a connection or link between two objects is called an edge. The first type of connection is a one-to-one connection in which we connect a single object type to another single object type.

Photos are posted by users, so every photo in our system should contain an edge connecting the photo to the user who posted it. Figure 4-1 shows a one-way connection between two types: Photo and User. The edge that connects the two nodes is called postedBy.

One-To-One Connection
Figure 4-1. One-to-one connection

Let’s see how we would define this in the schema:

type User {
    githubLogin: ID!
    name: String
    avatar: String
}

type Photo {
    id: ID!
    name: String!
    url: String!
    description: String
    created: DateTime!
    category: PhotoCategory!
    postedBy: User!
}

First, we’ve added a new type to our schema, the User. The users of the PhotoShare application are going to sign in via GitHub. When the user signs in, we obtain their githubLogin and use it as the unique identifier for their user record. Optionally, if they added their name or photo to GitHub, we will save that information under the fields name and avatar.

Next, we added the connection by adding a postedBy field to the photo object. Each photo must be posted by a user, so this field is set to the User! type; the exclamation point is added to make this field non-nullable.

One-to-Many Connections

It is a good idea to keep GraphQL services undirected when possible. This provides our clients with the ultimate flexibility to create queries because they can start traversing the graph from any node. All we need to do to follow this practice is provide a path back from User types to Photo types. This means that when we query a User, we should get to see all of the photos that particular user posted:

type User {
    githubLogin: ID!
    name: String
    avatar: String
    postedPhotos: [Photo!]!
}

By adding the postedPhotos field to the User type, we have provided a path back to the Photo from the user. The postedPhotos field will return a list of Photo types, those photos posted by the parent user. Because one user can post many photos, we’ve created a one-to-many connection. One-to-many connections, as shown in Figure 4-2, are common connections that are created when a parent object contains a field that lists other objects.

One-To-Many Connection
Figure 4-2. One-to-many connection

A common place to add one-to-many connections is in our root types. To make our photos or users available in a query, we need to define the fields of our Query root type. Let’s take a look at how we can add our new custom types to the Query root type:

type Query {
    totalPhotos: Int!
    allPhotos: [Photo!]!
    totalUsers: Int!
    allUsers: [User!]!
}

schema {
    query: Query
}

Adding the Query type defines the queries that are available in our API. In this example, we’ve added two queries for each type: one to deliver the total number of records available on each type, and another to deliver the full list of those records. Additionally, we’ve added the Query type to the schema as a file. This makes our queries available in our GraphQL API.

Now our photos and users can be queried with the following query string:

query {
    totalPhotos
    allPhotos {
        name
        url
    }
}

Many-to-Many Connections

Sometimes we want to connect lists of nodes to other lists of nodes. Our PhotoShare application will allow users to identify other users in each photo that they post. This process is called tagging. A photo can consist of many users, and a user can be tagged in many photos, as Figure 4-3 shows.

Many to Many Connection
Figure 4-3. Many-to-many connection

To create this type of connection, we need to add list fields to both the User and the Photo types.

type User {
    ...
    inPhotos: [Photo!]!
}

type Photo {
    ...
    taggedUsers: [User!]!
}

As you can see, a many-to-many connection consists of two one-to-many connections. In this case, a Photo can have many tagged users, and a User can be tagged in many photos.

Through types

Sometimes, when creating many-to-many relationships, you might want to store some information about the relationship itself. Because there is no real need for a through type in our photo sharing app, we are going to use a different example to define a through type, a friendship between users.

We can connect many users to many users by defining a field under a User that contains a list of other users:

type User {
    friends: [User!]!
}

Here, we’ve defined a list of friends for each user. Consider a case in which we wanted to save some information about the friendship itself, like how long users have known one another or where they met.

In this situation, we need to define the edge as a custom object type. We call this object a through type because it is a node that is designed to connect two nodes. Let’s define a through type called Friendship that we can use to connect two friends but also deliver data on how the friends are connected:

type User {
    friends: [Friendship!]!
}
type Friendship {
    friend_a: User!
    friend_b: User!
    howLong: Int!
    whereWeMet: Location
}

Instead of defining the friends field directly on a list of other User types, we’ve created a Friendship to connect the friends. The Friendship type defines the two connected friends: friend_a and friend_b. It also defines some detail fields about how the friends are connected: howLong and whereWeMet. The howLong field is an Int that will define the length of the friendship, and the whereWeMet field links to a custom type called Location.

We can improve upon the design of the Friendship type by allowing for a group of friends to be a part of the friendship. For example, maybe you met your best friends at the same time in first grade. We can allow for two or more friends to be a part of the friendship by adding a single field called friends:

    type Friendship {
        friends: [User!]!
        how_long: Int!
        where_we_met: Location
    }

We’ve only included one field for all of the friends in a Friendship. Now this type can reflect two or more friends.

Lists of Different Types

In GraphQL, our lists do not always need to return the same type. In Chapter 3, we introduced union types and interfaces, and we learned how to write queries for these types using fragments. Let’s take a look at how we can add these types to our schema.

Here, we will use a schedule as an example. You might have a schedule made up of different events, each requiring different data fields. For instance, the details about a study group meeting or a workout might be completely different, but you should be able to add both to a schedule. You can think of a daily schedule as a list of different types of activities.

There are two ways in which we can handle defining a schema for a schedule in GraphQL: unions and interfaces.

Union types

In GraphQL, a union type is a type that we can use to return one of several different types. Recall from Chapter 3 how we wrote a query called schedule that queried an agenda and returned different data when the agenda item was a workout than when it was a study group. Let’s take a look at it again here:

query schedule {
    agenda {
        ...on Workout {
            name
            reps
        }
        ...on StudyGroup {
            name
            subject
            students
        }
    }
}

In the student’s daily agenda, we could handle this by creating a union type called AgendaItem:

union AgendaItem = StudyGroup | Workout

type StudyGroup {
    name: String!
    subject: String
    students: [User!]!
}

type Workout {
    name: String!
    reps: Int!
}

type Query {
    agenda: [AgendaItem!]!
}

AgendaItem combines study groups and workouts under a single type. When we add the agenda field to our Query, we are defining it as a list of either workouts or study groups.

It is possible to join as many types as we want under a single union. Simply separate each type with a pipe:

union = StudyGroup | Workout | Class | Meal | Meeting | FreeTime

Interfaces

Another way of handling fields that could contain multiple types is to use an interface. Interfaces are abstract types that can be implemented by object types. An interface defines all of the fields that must be included in any object that implements it. Interfaces are a great way to organize code within your schema. This ensures that certain types always include specific fields that are queryable no matter what type is returned.

In Chapter 3, we wrote a query for an agenda that used an interface to return fields on different items in a schedule. Let’s review that here:

query schedule {
  agenda {
    name
    start
    end
    ...on Workout {
      reps
    }
  }
}

Here is what it might look like to query an agenda that implemented interfaces. For a type to interface with our schedule, it must contain specific fields that all agenda items will implement. These fields include name, start, and end times. It doesn’t matter what type of schedule item you have, they all need these details in order to be listed on a schedule.

Here is how we would implement this solution in our GraphQL schema:

scalar DataTime

interface AgendaItem {
    name: String!
    start: DateTime!
    end: DateTime!
}

type StudyGroup implements AgendaItem {
    name: String!
    start: DateTime!
    end: DateTime!
    participants: [User!]!
    topic: String!
}

type Workout implements AgendaItem {
    name: String!
    start: DateTime!
    end: DateTime!
    reps: Int!
}

type Query {
    agenda: [AgendaItem!]!
}

In this example, we create an interface called AgendaItem. This interface is an abstract type that other types can implement. When another type implements an interface, it must contain the fields defined by the interface. Both StudyGroup and Workout implement the AgendaItem interface, so they both need to use the name, start, and end fields. The query agenda returns a list of AgendaItem types. Any type that implements the AgendaItem interface can be returned in the agenda list.

Also notice that these types can implement other fields, as well. A StudyGroup has a topic and a list of participants, and a Workout still has reps. You can select these additional fields in a query by using fragments.

Both union types and interfaces are tools that you can use to create fields that contain different object types. It’s up to you to decide when to use one or the other. In general, if the objects contain completely different fields, it is a good idea to use union types. They are very effective. If an object type must contain specific fields in order to interface with another type of object, you will need to use an interface rather than a union type.

Arguments

Arguments can be added to any field in GraphQL. They allow us to send data that can affect outcome of our GraphQL operations. In Chapter 3, we looked at user arguments within our queries and mutations. Now, let’s take a look at how we would define arguments in our schema.

The Query type contains fields that will list allUsers or allPhotos, but what happens when you want to select only one User or one Photo? You would need to supply some information on the one user or photo that you would like to select. You can send that information along with my query as an argument:

type Query {
    ...
    User(githubLogin: ID!): User!
    Photo(id: ID!): Photo!
}

Just like a field, an argument must have a type. That type can be defined using any of the scalar types or object types that are available in our schema. To select a specific user, we need to send that user’s unique githubLogin as an argument. The following query selects only MoonTahoe’s name and avatar:

query {
    User(githubLogin: "MoonTahoe") {
        name
        avatar
    }
}

To select details about an individual photo, we need to supply that photo’s ID:

query {
    Photo(id: "14TH5B6NS4KIG3H4S") {
        name
        description
        url
    }
}

In both cases, arguments were required to query details about one specific record. Because these arguments are required, they are defined as non-nullable fields. If we do not supply the id or githubLogin with these queries, the GraphQL parser will return an error.

Filtering Data

Arguments do not need to be non-nullable. We can add optional arguments using nullable fields. This means that we can supply arguments as optional parameters when we execute query operations. For example, we could filter the photo list that is returned by the allPhotos query by photo category:

type Query {
    ...
    allPhotos(category: PhotoCategory): [Photo!]!
}

We have added an optional category field to the allPhotos query. The category must match the values of the enumeration type PhotoCategory. If a value is not sent with the query, we can assume that this field will return every photo. However, if a category is supplied, we should get a filtered list of photos in the same category:

query {
    allPhotos(category: "SELFIE") {
        name
        description
        url
    }
}

This query would return the name, description, and url of every photo categorized as a SELFIE.

Data paging

If our PhotoShare application is successful, which it will be, it will have a lot of Users and Photos. Returning every User or every Photo in our application might not be possible. We can use GraphQL arguments to control the amount of data that is returned from our queries. This process is called data paging because a specific number of records are returned to represent one page of data.

To implement data paging, we are going to add two optional arguments: first to collect the number of records that should be returned at once in a single data page, and start to define the starting position or index of the first record to return. We can add these arguments to both of our list queries:

type Query {
    ...
    allUsers(first: Int=50 start: Int=0): [User!]!
    allPhotos(first: Int=25 start: Int=0): [Photo!]!
}

In the preceding example, we have added optional arguments for first and start. If the client does not supply these arguments with the query, we will use the default values provided. By default, the allUsers query returns only the first 50 users, and the allPhotos query returns only the first 25 photos.

The client can query a different range of either user or photos by supplying values for these arguments. For example, if we want to select users 90 through 100, we could do so by using the following query:

query {
    allUsers(first: 10 start: 90) {
        name
        avatar
    }
}

This query selects only 10 years starting at the 90th user. It should return the name and avatar for that specific range of users. We can calculate the total number of pages that are available on the client by dividing the total number of items by the size of one page of data:

pages = pageSize/total

Sorting

When querying a list of data, we might also want to define how the returned list of data should be sorted. We can use arguments for this, as well.

Consider a scenario in which we wanted to incorporate the ability to sort any lists of Photo records. One way to tackle this challenge is to create enums that specify which fields can be used to sort Photo objects and instructions for how to sort those fields:

enum SortDirection {
    ASCENDING
    DESCENDING
}

enum SortablePhotoField {
    name
    description
    category
    created
}

Query {
    allPhotos(
        sort: SortDirection = DESCENDING
        sortBy: SortablePhotoField = created
    ): [Photo!]!
}

Here, we’ve added the arguments sort and sortBy to the allPhotos query. We created an enumeration type called SortDirection that we can use to limit the values of the sort argument to ASCENDING or DESCENDING. We’ve also created another enumeration type for SortablePhotoField. We don’t want to sort photos on just any field, so we’ve restricted sortBy values to include only four of the photo fields: name, description, category, or created (the date and time that the photo was added). Both sort and sortBy are optional arguments, so they default to DESCENDING and created if either of the arguments are not supplied.

Clients can now control how their photos are sorted when they issue an allPhotos query:

query {
    allPhotos(sortBy: name)
}

This query will return all of the photos sorted by descending name.

So far, we’ve added arguments only to fields of the Query type, but it is important to note that you can add arguments to any field. We could add the filtering, sorting, and paging arguments to the photos that have been posted by a single user:

type User {
    postedPhotos(
        first: Int = 25
        start: Int = 0
        sort: SortDirection = DESCENDING
        sortBy: SortablePhotoField = created
        category: PhotoCategory
    ): [Photo!]

Adding pagination filters can help reduce the amount of data a query can return. We discuss the idea of limiting data at greater length in Chapter 7.

Mutations

Mutations must be defined in the schema. Just like queries, mutations also are defined in their own custom object type and added to the schema. Technically, there is no difference between how a mutation or query is defined in your schema. The difference is in intent. We should create mutations only when an action or event will change something about the state of our application.

Mutations should represent the verbs in your application. They should consist of the things that users should be able to do with your service. When designing your GraphQL service, make a list of all of the actions that a user can take with your application. Those are most likely your mutations.

In the PhotoShare app, users can sign in with GitHub, post photos, and tag photos. All of these actions change something about the state of the application. After they are signed in with GitHub, the current users accessing the client will change. When a user posts a photo, there will be an additional photo in the system. The same is true for tagging photos. New photo-tag data records are generated each time a photo is tagged.

We can add these mutations to our root mutation type in our schema and make them available to the client. Let’s begin with our first mutation, postPhoto:

type Mutation {
    postPhoto(
        name: String!
        description: String
        category: PhotoCategory=PORTRAIT
    ): Photo!
}

schema {
    query: Query
    mutation: Mutation
}

Adding a field under the Mutation type called postPhoto makes it possible for users to post photos. Well, at least it makes it possible for users to post metadata about photos. We handle uploading the actual photos in Chapter 7.

When a user posts a photo, at a bare minimum the photo’s name is required. The description and category are optional. If a category argument is not supplied, the posted photo will be defaulted to PORTRAIT. For example, a user can post a photo by sending the following mutation:

mutation {
    postPhoto(name: "Sending the Palisades") {
        id
        url
        created
        postedBy {
            name
        }
    }
}

After the user posts a photo, they can select information about the photo that they just posted. This is good because some of the record details about the new photo will be generated on the server. The ID for our new photo will be created by the database. The photo’s url will automatically be generated. The photo will also be timestamped with the date and time that the photo was created. This query selects all of these new fields after a photo has been posted.

Additionally, the selection set includes information about the user who posted the photo. A user must be signed in to post a photo. If no user is presently signed in, this mutation should return an error. Assuming that a user is signed in, we can obtain details about who posted the photo via the postedBy field. In Chapter 5, we cover how to authenticate an authorized user by using an access token.

Input Types

As you might have noticed, the arguments for a couple of our queries and mutations are getting quite lengthy. There is a better way to organize these arguments using input types. An input type is similar to the GraphQL object type except it is used only for input arguments.

Let’s improve the postPhoto mutation using an input type for our arguments:

input PostPhotoInput {
  name: String!
  description: String
  category: PhotoCategory=PORTRAIT
}

type Mutation {
    postPhoto(input: PostPhotoInput!): Photo!
}

The PostPhotoInput type is like an object type, but it was created only for input arguments. It requires a name and the description, but category fields are still optional. Now when sending the postPhoto mutation, the details about the new photo need to be included in one object:

mutation newPhoto($input: PostPhotoInput!) {
    postPhoto(input: $input) {
        id
        url
        created
    }
}

When we create this mutation, we set the $input query variable’s type to match our PostPhotoInput! input type. It is non-nullable because at minimum we need to access the input.name field to add a new photo. When we send the mutation, we need to supply the new photo data in our query variables nested under the input field:

{
    "input": {
        "name": "Hanging at the Arc",
        "description": "Sunny on the deck of the Arc",
        "category": "LANDSCAPE"
    }
}

Our input is grouped together in a JSON object and sent along with the mutation in the query variables under the “input” key. Because the query variables are formatted as JSON, the category needs to be a string that matches one of the categories from the PhotoCategory type.

Input types are key to organizing and writing a clear GraphQL schema. You can use input types as arguments on any field. You can use them to improve both data paging and data filtering in applications.

Let’s take a look at how we can organize and reuse all of our sorting and filtering fields by using input types:

input PhotoFilter {
    category: PhotoCategory
    createdBetween: DateRange
    taggedUsers: [ID!]
    searchText: String
}

input DateRange {
    start: DateTime!
    end: DateTime!
}

input DataPage {
    first: Int = 25
    start: Int = 0
}

input DataSort {
    sort: SortDirection = DESCENDING
    sortBy: SortablePhotoField = created
}

type User {
    ...
    postedPhotos(filter:PhotoFilter paging:DataPage sorting:DataSort): [Photo!]!
    inPhotos(filter:PhotoFilter paging:DataPage sorting:DataSort): [Photo!]!
}

type Photo {
    ...
    taggedUsers(sorting:DataSort): [User!]!
}

type Query {
    ...
    allUsers(paging:DataPage sorting:DataSort): [User!]!
    allPhotos(filter:PhotoFilter paging:DataPage sorting:DataSort): [Photo!]!
}

We’ve organized numerous fields under input types and have reused those fields as arguments across our schema.

The PhotoFilter input types contain optional input fields that allow the client to filter a list of photos. The PhotoFilter type includes a nested input type, DateRange, under the field createdBetween. DateRange must include start and end dates. Using the PhotoFilter, we also can filter photos by category, search string, or taggedUsers. We add all of these filter options to every field that returns a list of photos. This gives the client a lot of control over which photos are returned from every list.

Input types have also been created for paging and sorting. The DataPage input type contains the fields needed to request a page of data and the DataSort input type contains our sorting fields. These input types have been added to every field in our schema that returns a list of data.

We could write a query that accepts some pretty complex input data using the available input types:

query getPhotos($filter:PhotoFilter $page:DataPage $sort:DataSort) {
    allPhotos(filter:$filter paging:$page sorting:$sort) {
        id
        name
        url
    }
}

This query optionally accepts arguments for three input types: $filter, $page, and $sort. Using query variables, we can send some specific details about what photos we would like to return:

{
    "filter": {
        "category": "ACTION",
        "taggedUsers": ["MoonTahoe", "EvePorcello"],
        "createdBetween": {
            "start": "2018-11-6",
            "end": "2018-5-31"
        }
    },
    "page": {
        "first": 100
    }
}

This query will find all of the ACTION photos for which GitHub users MoonTahoe and EvePorcello are tagged between November 6th and May 31st, which happens to be ski season. We also ask for the first 100 photos with this query.

Input types help us organize our schema and reuse arguments. They also improve the schema documentation that GraphiQL or GraphQL Playground automatically generates. This will make your API more approachable and easier to learn and digest. Finally, you can use input types to give the client a lot of power to execute organized queries.

Return Types

All of the fields in our schema have been returning our main types, User and Photo. But sometimes we need to return meta information about queries and mutations in addition to the actual payload data. For example, when a user has signed in and been authenticated, we need to return a token in addition to the User payload.

To sign in with GitHub OAuth, we must obtain an OAuth code from GitHub. We discuss setting up your own GitHub OAuth account and obtaining the GitHub code in “GitHub Authorization”. For now, let’s assume that you have a valid GitHub code that you can send to the githubAuth mutation to sign in a user:

type AuthPayload {
    user: User!
    token: String!
}

type Mutation {
    ...
    githubAuth(code: String!): AuthPayload!
}

Users are authenticated by sending a valid GitHub code to the githubAuth mutation. If successful, we will return a custom object type that contains both information about the user that was successfully signed in as well as a token that can be used to authorize further queries and mutations including the postPhoto mutation.

You can use custom return types on any field for which we need more than simple payload data. Maybe we want to know how long it takes for a query to deliver a response, or how many results were found in a particular response in addition to the query payload data. You can handle all of this by using a custom return type.

At this point, we have introduced all of the types that are available to you when creating GraphQL schemas. We’ve even taken a bit of time to discuss techniques that can help you improve your schema design. But there is one last root object type that we need to introduce—the Subscription type.

Subscriptions

Subscription types are no different than any other object type in the GraphQL schema definition language. Here, we define the available subscriptions as fields on a custom object type. It will be up to us to make sure the subscriptions implement the PubSub design pattern along with some sort of real-time transport when we build the GraphQL service later in Chapter 7.

For example, we can add subscriptions that allow our clients to listen for the creation of new Photo or User types:

type Subscription {
    newPhoto: Photo!
    newUser: User!
}

schema {
    query: Query
    mutation: Mutation
    subscription: Subscription
}

Here, we create a custom Subscription object that contains two fields: newPhoto and newUser. When a new photo is posted, that new photo will be pushed to all of the clients who have subscribed to the newPhoto subscription. When a new user has been created, their details are pushed to every client who is listening for new users.

Just like queries or mutations, subscriptions can take advantage of arguments. Suppose that we want to add filters to the newPhoto subscription that would cause it to listen only for new ACTION photos:

type Subscription {
    newPhoto(category: PhotoCategory): Photo!
    newUser: User!
}

When users subscribe to the newPhoto subscription, they now have the option to filter the photos that are pushed to this subscription. For example, to filter for only new ACTION photos, clients could send the following operation to our GraphQL API:

subscription {
    newPhoto(category: "ACTION") {
        id
        name
        url
        postedBy {
            name
        }
    }
}

This subscription should return details for only ACTION photos.

A subscription is a great solution when it’s important to handle data in real time. In Chapter 7, we talk more about subscription implementation for all of your real-time data handling needs.

Schema Documentation

Chapter 3 explains how GraphQL has an introspection system that can inform you as to what queries the server supports. When writing a GraphQL schema, you can add optional descriptions for each field that will provide additional information about the schema’s types and fields. Providing descriptions can make it easier for your team, yourself, and other users of the API to understand your type system.

For example, let’s add comments to the User type in our schema:

"""
A user who has been authorized by GitHub at least once
"""
type User {

    """
    The user's unique GitHub login
    """
    githubLogin: ID!

    """
    The user's first and last name
    """
    name: String

    """
    A url for the user's GitHub profile photo
    """
    avatar: String

    """
    All of the photos posted by this user
    """
    postedPhotos: [Photo!]!

    """
    All of the photos in which this user appears
    """
    inPhotos: [Photo!]!

}

By adding three quotation marks above and below your comment on each type or field, you provide users with a dictionary for your API. In addition to types and fields, you can also document arguments. Let’s look at the postPhoto mutation:

Replace with:

  type Mutation {
    """
    Authorizes a GitHub User
    """
    githubAuth(
      "The unique code from GitHub that is sent to authorize the user"
      code: String!
    ): AuthPayload!
  }

The argument comments share the name of the argument and whether the field is optional. If you’re using input types, you can document these like any other type:

"""
The inputs sent with the postPhoto Mutation
"""
input PostPhotoInput {
  "The name of the new photo"
  name: String!
  "(optional) A brief description of the photo"
  description: String
  "(optional) The category that defines the photo"
  category: PhotoCategory=PORTRAIT
}

postPhoto(
      "input: The name, description, and category for a new photo"
      input: PostPhotoInput!
): Photo!

All of these documentation notes are then listed in the schema documentation in the GraphQL Playground or GraphiQL as shown in Figure 4-4. Of course, you can also issue introspection queries to find the descriptions of these types.

postPhoto Documentation
Figure 4-4. postPhoto Documentation

At the heart of all GraphQL projects is a solid, well-defined schema. This serves as a roadmap and a contract between the frontend and backend teams to ensure that the product built always serves the schema.

In this chapter, we created a schema for our photo-sharing application. In the next three chapters, we show you how to build a full-stack GraphQL application that fulfills the contract of the schema we just created.

Get Learning GraphQL now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.