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.
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.
list declaration | definition |
---|---|
|
A list of nullable integer values |
|
A list of non-nullable integer values |
|
A non-nullable list of nullable integer values |
|
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
.
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.
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.
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 enum
s 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.
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.