Graphql In Action
Customizing Organizing Operations
This is still a work in progress. New content is synced here as it gets ready.

Customizing and organizing GraphQL operations

This chapter covers

  • Using arguments to customize what a request field returns

  • Customizing response property names with aliases

  • Describing runtime executions with directives

  • Reducing duplicated text with fragments

  • Composing queries and separating data requirement responsibilities

In any nontrivial application, you have to do many things beyond asking the server a direct, simple, single-value question. Data fetching is usually coupled with variables and meta-questions about the structure of the response. You often need to modify the data returned by the server to make it suitable for your application. Sometimes you have to remove parts of the data or go back to the server and ask for other parts that are required based on conditions in your application. You also need a way to organize big requests and categorize them to know which part of your application is responsible for each part of your requests. Luckily, the GraphQL language offers many built-in features you can use to do all of this and much more. These customizing and organizing features are what this chapter is all about.

1. Customizing fields with arguments

The fields in a GraphQL operation are similar to functions. They map input to output. A function input is received as a list of argument values. Just like functions, we can pass any GraphQL field a list of argument values. A GraphQL schema on the backend can access these values and use them to customize the response it returns for that field.

Let’s look at use cases for these field arguments and some examples used by the GitHub GraphQL API (az.dev/github-api).

1.1. Identifying a single record to return

Every API request that asks for a single record from a collection needs to specify an identifier for that record. This identifier is usually associated with a unique identifier for that record in the server’s database, but it can also be anything else that can uniquely identify the record.

For example, if you ask an API for information on a single user, you usually send along with your request the ID of the user you are interested in. You can also send their email address, username, or Facebook ID connection if, for example, you are logging them in through a Facebook button.

Here is an example query that asks for information about the user whose email address is [email protected]

Listing 3. 1. Using field arguments
query UserInfo {
  user(email: "[email protected]") {
    firstName
    lastName
    userName
  }
}

The email part inside the user field is called a field argument.

Note that for an API field representing a single record, the argument value you pass to identify that record must be a unique value on that field record in the database. For example, you cannot pass the person’s full name to identify their user record because the database might list many people who have the same name.

However, you can pass multiple arguments to identify the user. For example, you can pass a full name and an address to uniquely identify a single person.

Examples of single-record fields are popular. Some GraphQL APIs even have a single-record field for every object in the system. This is commonly known in the GraphQL world as a Node interface: a concept popularized by the Relay framework (which also originated at Facebook). With a Node interface, you can look up any node in the data graph by its unique global system-wide ID. Then, based on what that node is, you can use an inline fragment to specify the properties on that node that you are interested in seeing in the response.

Listing 3. 2. Identifying a single global node
query NodeInfo {
  node(id: "A-GLOBALLY-UNIQUE-ID-HERE") {
    ...on USER {
      firstName
      lastName
      userName
      email
    }
  }
}

See section 4 later in this chapter for more details about the inline-fragment in listing 3.2.

In the GitHub API, some examples of single-record fields are user, repository, project, organization, and many more. Here is an example to read information about the jsComplete organization, which hosts all open source resources for jsComplete.com.

Listing 3. 3. One organization’s information (az.dev/gia)
query OrgInfo {
  organization(login: "jscomplete") {
    name
    description
    websiteUrl
  }
}

1.2. Limiting the number of records returned by a list field

When you ask for a list of records from a collection, a good API will always ask you to provide a limit. How many records are you interested in?

It is usually a bad idea to leave a general API capability that enables listing records in a collection without a limit. You do not want a client to be able to fetch more than a few hundred records at a time, because that would put your API server at risk of resource exhaustion and does not scale well. This is exactly why the GitHub API requires the use of an argument like first (or last) when you ask it for a list of records. Go ahead and try to ask for all the repositories under the jsComplete organization. You can use the repositories field within the organization field in the OrgInfo query in listing 3.3. You should see that GitHub asks for a first or last value, as shown in figure 3.1.

ch03 fig 01 gqlia
Figure 3. 1. The repositories field requires a first or last argument.

Since any list of records in a database has a certain order, you can limit your request results using either end of that order. If you are interested in 10 records, you can get the first 10 records or the last 10 records using these arguments.

Here is the query you can use to retrieve the first 10 repositories under the jsComplete organization.

Listing 3. 4. First 10 repos under the organization (az.dev/gia)
query First10Repos {
  organization(login: "jscomplete") {
    name
    description
    websiteUrl
    repositories(first: 10) {
      nodes {
        name
      }
    }
  }
}

By default, the GitHub API orders the repositories in ascending order by date of creation. You can customize that ordering logic with another field argument.

1.3. Ordering records returned by a list field

In the previous example, the GitHub API ordered the list of repositories under the jsComplete organization by the CREATED_AT repository order field. The API supports many other order fields, including UPDATED_AT, PUSHED_AT, NAME, and STARGAZERS.

Here is a query to retrieve the first 10 repositories when they are ordered alphabetically by name.

Listing 3. 5. First 10 repos under an organization (az.dev/gia)
query orgReposByName {
  organization(login: "jscomplete") {
    repositories(first: 10, orderBy: { field: NAME, direction: ASC }) {
      nodes {
        name
      }
    }
  }
}

Can you use the GitHub field arguments you learned about find the top-10 most popular repositories under the jsComplete organization? Base a repository’s popularity on the number of stars it has.

Here is one query you can use to do that.

Listing 3. 6. 10 most popular repos under an organization (az.dev/gia)
query OrgPopularRepos {
  organization(login: "jscomplete") {
    repositories(first: 10, orderBy: { field: STARGAZERS, direction: DESC }) {
      nodes {
        name
      }
    }
  }
}

1.4. Paginating through a list of records

When you need to retrieve a page of records, in addition to specifying a limit, you need to specify an offset. In the GitHub API, you can use the field arguments after and before to offset the results returned by the arguments first and last, respectively.

To use these arguments, you need to work with node identifiers, which are different than database record identifiers. The pagination interface that the GitHub API uses is called the Connection interface (which originated from the Relay framework as well). In that interface, every record is identified by a node field (similar to the Node interface) using a cursor field. The cursor is basically the ID field for each node, and it is the field we use with the before and after arguments.

To work with every node’s cursor next to that node’s data, the Connection interface adds a new parent to the node concept called an edge. The edges field represents a list of paginated records, and every object in that list holds a unique cursor value.

Here is a query that includes cursor values through the edges field.

Listing 3. 7. Working with cursors under edges (az.dev/gia)
query OrgRepoConnectionExample {
  organization(login: "jscomplete") {
    repositories(first: 10, orderBy: { field: CREATED_AT, direction: ASC }) {
      edges {
        cursor
        node {
          name
        }
      }
    }
  }
}

Note that within an edges field, we now ask about a single node field because the list is no longer a list of nodes but rather a list of edges where each edge is a node + cursor.

Now that you can see the string values of these cursors, you can use them as the after and before arguments to fetch extra pages of data. For example, to fetch the second page of the repositories under the jsComplete organization, you need to identify the cursor of the last repository on the first page (which was advanced-nodejs when I tested these queries) and use that cursor value as the after value.

Listing 3. 8. Fetching the second page of repos (az.dev/gia)
query OrgRepoConnectionExample2 {
  organization(login: "jscomplete") {
    repositories(
      first: 10,
      after: "Y3Vyc29yOnYyOpK5MjAxNy0wMS0yMVQwODo1NTo0My0wODowMM4Ev4A3",
      orderBy: { field: CREATED_AT, direction: ASC }
    ) {
      edges {
        cursor
        node {
          name
        }
      }
    }
  }
}

The introduction of the edges field also allows the addition of metadata about the list. For example, on the same level where we ask for a list of edges, we can also ask how many total records this relation has and whether there are more records to fetch after the current page. Here is the previous query modified to show some metadata about the relation.

Listing 3. 9. Meta pagination information (az.dev/gia)
query OrgReposMetaInfoExample {
  organization(login: "jscomplete") {
    repositories(
      first: 10,
      after: "Y3Vyc29yOnYyOpK5MjAxNy0wMS0yMVQwODo1NTo0My0wODowMM4Ev4A3",
      orderBy: { field: STARGAZERS, direction: DESC }
    ) {
      totalCount
      pageInfo {
        hasNextPage
      }
      edges {
        cursor
        node {
          name
        }
      }
    }
  }
}

Since the jsComplete organization has more than 20 repositories (2 pages, in this example), the hasNextPage field is true. When you fetch the very last page, hasNextPage will return false, indicating that there is no more data to fetch. This is much better than having to do an extra empty page fetch to conclude that you have reached the end of the paginated data.

1.5. Searching and filtering

A field argument in GraphQL can be used to provide filtering criteria or search terms to limit the results returned by a list. Let’s see examples for both features.

In GitHub, a repository can have a list of projects to manage any work related to that repository. For example, the Twitter Bootstrap repository on GitHub started using a project per release to manage all the issues related to a single release. Here is a query that uses a search term within the projects relation to return the Twitter Bootstrap projects that start with "v4.1".

Listing 3. 10. Using field arguments to search (az.dev/gia)
query SearchExample {
  repository(owner: "twbs", name: "bootstrap") {
    projects(search: "v4.1", first: 10) {
      nodes {
        name
      }
    }
  }
}

Note that the projects field also implements the Connection interface.

Some fields allow you to filter the returned list by certain properties of that field. For example, under the GitHub viewer field, you can see a list of all your repositories. By default, that list includes all the repositories that you own or can contribute to. To list only the repositories that you own, you can use the affiliations field argument.

Listing 3. 11. Using field arguments to filter (az.dev/gia)
query FilterExample {
  viewer {
    repositories(first: 10, affiliations: OWNER) {
      totalCount
      nodes {
        name
      }
    }
  }
}

1.6. Providing input for mutations

The field arguments concept is what GraphQL mutations use to accept the mutation operation’s input. In the previous chapter, we used the following mutation example to add a star to the GraphQL-In-Action repository under the jsComplete organization.

Listing 3. 12. Arguments to provide mutation input (az.dev/gia)
mutation StarARepo {
  addStar(input: { starrableId: "MDEwOlJlcG9zaXRvcnkxMjU2ODEwMDY=" }) {
    starrable {
      stargazers {
        totalCount
      }
    }
  }
}

The input value in that mutation is also a field argument. It is a required argument. You cannot perform a GitHub mutation operation without an input object. All GitHub API mutations use this single required input field argument that represents an object. To perform a mutation, you pass the various input values as key/value pairs on that input object.

Not all arguments are required. A GraphQL API can control which arguments are required and which are optional.

There are many more cases where a field argument can be useful. Explore the GitHub API and other publicly available GraphQL APIs for more useful patterns for field arguments.

2. Renaming fields with aliases

The alias feature in a GraphQL operation is very simple but powerful because it allows you to customize a response coming from the server through the request itself. By using aliases, you can minimize any post-response processing on the data.

Let me explain this with an example. Let’s say you are developing the profile page in GitHub. Here is a query to retrieve partial profile information for a GitHub user.

Listing 3. 13. Profile information query (az.dev/gia)
query ProfileInfo {
  user(login: "samerbuna") {
    name
    company
    bio
  }
}

You get a simple user object in the response (see figure 3.2).

ch03 fig 02 gqlia
Figure 3. 2. The ProfileInfo query asking for a company field

Now an application UI can use this user object in the query’s response to substitute for the values in a UI template. However, suppose you have just discovered a mismatch between the structure of your response and the structure the application UI is using for the user object. The application UI was developed to expect a companyName field on a user instead of a company field (as found in the API response). What do you do? Assume that changing the application UI code itself is not an option.

If you do not have the option to use an alias (I will show you how in a bit), your can process the response every time you use the response object. You need to transform the user object from the response into a new object with the right structure. This can be costly if the structure you are working with deep and has multiple levels.

Luckily, in GraphQL, the awesome alias feature lets us declaratively instruct the API server to return fields using different names. All you need to do is specify an alias for that field, which you can do using this syntax:

aliasName: fieldName

Just prefix any field name with an alias, and the server will return that field renamed using your alias. There is no need to process the response object. For the example in listing 3.13, all you need to do is specify a companyName alias.

Listing 3. 14. Profile information query with an alias (az.dev/gia)
query ProfileInfoWithAlias {
  user(login: "samerbuna") {
    name
    companyName: company
    bio
  }
}

This gives a response that is ready for you to plug into the application UI (see figure 3.3).

ch03 fig 03 gqlia
Figure 3. 3. Using the GraphQL alias feature to get a companyName field from the server

3. Customizing responses with directives

Sometimes, the customization you need on a server response goes beyond simple renaming of fields. You may need to conditionally include (or exclude) branches of data in your responses. This is where the directives feature of GraphQL can be helpful.

A directive in a GraphQL request is a way to provide a GraphQL server with additional information about the execution and type validation behavior of a GraphQL document. It is essentially a more powerful version of field arguments: you can use directives to conditionally include or exclude the entire field. In addition to fields, directives can be used with fragments and top-level operations.

A directive is any string in a GraphQL document that begins with the @ character. Every GraphQL schema has three built-in directives: @include, @skip, and @deprecated. Some schemas have more directives. You can use this introspective query to see the list of directives supported by a schema.

Listing 3. 15. All the supported directives in a schema (az.dev/gia)
query AllDirectives {
  __schema {
    directives {
      name
      description
      locations
      args {
        name
        description
        defaultValue
      }
    }
  }
}

This query shows the name and description of each directive and includes an array of all possible locations where that directive can be used (figure 3.4). In addition, it lists all arguments supported by that directive. Each directive can optionally receive a list of arguments, and just like field arguments, some argument values may be required by the API server. The response to the previous query should show that in any GraphQL schema, both the @include and @skip directives have the argument if. The @deprecated directive has the argument reason.

ch03 fig 04 gqlia
Figure 3. 4. List of all directives supported by a schema

The list of locations in the previous query’s response is also important. Directives can be used only in the locations they are declared to belong to. For example, the @include and @skip directives can only be used after fields or fragments. You cannot use them at the top level of an operation. Similarly, the @deprecated directive can only be used after field definitions or ENUM values when defining a GraphQL service schema.

An ENUM (enumerated) type represents a set of possible unique values. We’ll see an example in the next chapter.

Since directives are usually used with arguments, they are often paired with query variables to have them sent with a dynamic value. We saw some examples of variables in the previous chapter, but let me remind you about them.

3.1. Variables and input values

A variable is simply any name in the GraphQL document that begins with a $ sign: for example, $login or $showRepositories. The name after the $ sign can be anything.

We use variables to make GraphQL operations generically reusable and avoid having to hardcode values and concatenate strings.

To use a variable in a GraphQL operation, you first need to define its type. You do that by providing arguments to any named operation. For example, let’s take the query example that we used to read information about the jsComplete organization on GitHub. Instead of hardcoding the login value (as we did before), let’s now use a variable. The operation must have a name, and then we can use that name’s arguments to define any variables. Let’s call the variable $orgLogin. It should be a required string. You can see the type of the arguments using the Docs explorer. Look up the organization field to see the types of its login argument. You can also click the organization field in the query while holding the Command key (Ctrl in Windows).

As you can see in figure 3.5, the login argument has a type of String!. The trailing exclamation mark on that type is GraphQL’s way of labeling the argument value as required. A value for this login argument must be specified — it cannot be null.

ch03 fig 05 gqlia
Figure 3. 5. Looking up one field’s documentation in a schema

Now we can use the same syntax to define the new variable. The type for $orgLogin should match the type of the argument where it is going to be used. Here is the same query written with this new $orgLogin variable.

Listing 3. 16. Using variables for argument values (az.dev/gia)
query OrgInfo($orgLogin: String!) {
  organization(login: $orgLogin) {
    name
    description
    websiteUrl
  }
}

Note that on the first line, the query specifies that $orgLogin is also a String!.

You cannot execute the query in listing 3.16 as is. If you try, the GraphQL server will return an error. Since we used a variable, we must give the executor on the server the value that we wish to use as that variable. In GraphiQL, we do that using the variables editor, which is in the lower-left corner. In that editor, you write a JSON object that represents all variables you want to send to the executor along with the operation.

Since we used only one variable, the JSON object for that is

{
  "orgLogin": "jscomplete"
}

Now you can execute the query with different JSON objects, making it reusable for different organizations (see figure 3.6).

ch03 fig 06 gqlia
Figure 3. 6. Variables make a GraphQL query reusable.

A variable like $orgLogin can also have a default value, in which case it does not need the trailing exclamation mark. You specify the default value using an equal sign after the type of the variable. For example, the previous query can have the value "jscomplete" as the default value of $orgLogin using this syntax.

Listing 3. 17. Using default values for variables (az.dev/gia)
query OrgInfoWithDefault($orgLogin: String = "jscomplete") {
  organization(login: $orgLogin) {
    name
    description
    websiteUrl
  }
}

You can execute this OrgInfoWithDefault query without passing a JSON object for variables. The query will use the default value in that case. If you pass a JSON object with a value for "orgLogin", that value will override the default value.

Variables can be used in fields and directives to make them accept input values of various literal primitives. An input value can be scalar, like Int, Float, String, Boolean, or Null. It can also be an ENUM value, a list, or an object. The $orgLogin variable represents a scalar string input value for the login argument within the organization field. Read the various GraphQL operation examples we have seen so far and try to identify more input values. For example, try to find where we used an object as an input value.

Now that we know how to define and use variables and values, let’s use them with directives.

3.2. The @include directive

The @include directive can be used after fields (or fragments) to provide a condition (using its if argument). That condition controls whether the field (or fragment) should be included in the response. The use of the @include directive looks like this:

fieldName @include(if: $someTest)

This says to include the field when the query is executed with $someTest set to true and not to include the field when $someTest is set to false. Let’s look at an example from the GitHub API.

Building on the previous OrgInfo query example, let’s assume that we want to conditionally include an organization’s websiteUrl based on whether we are showing full or partial details in the UI. Let’s design a Boolean variable to represent this flag and call it $fullDetails.

This new $fullDetails variable will be required because we are about to use it with a directive. The first line of the OrgInfo query needs to be changed to add the type of $fullDetails:

query OrgInfo($orgLogin: String!, $fullDetails: Boolean!) {

Now we want to include the websiteUrl only when we execute the OrgInfo query with $fullDetails set to true. A simple use of the @include directive can do that. The if argument value in this case will be the $fullDetails variable. Here is the full query.

Listing 3. 18. The @include directive (az.dev/gia)
query OrgInfo($orgLogin: String!, $fullDetails: Boolean!) {
  organization(login: $orgLogin) {
    name
    description
    websiteUrl @include(if: $fullDetails)
  }
}

Go ahead and test this query by executing it with $fullDetails set to true and false. You will see that the response honors that Boolean value and uses it to include or exclude websiteUrl from the response object (see figure 3.7).

ch03 fig 07 gqlia
Figure 3. 7. Using the @include directive with a variable

3.3. The @skip directive

This directive is simply the inverse of the @include directive. Just like the @include directive, it can be used after fields (or fragments) to provide a condition (using its if argument). The condition controls whether the field (or fragment) should be excluded in the response. The use of the @skip directive looks like this:

fieldName @skip(if: $someTest)

This means to exclude the field when the query is executed with $someTest set to true and include the field when $someTest is set to false. This directive is useful to avoid negating a variable value, especially if that variable has a negative name already.

Suppose that instead of designing the Boolean variable to be $fullDetails, we decide to name it $partialDetails. Instead of inverting that variable value in the JSON values object, we can use the @skip directive to use the $partialDetails value directly. The OrgInfo query becomes the following.

Listing 3. 19. The @skip directive (az.dev/gia}
query OrgInfo($orgLogin: String!, $partialDetails: Boolean!) {
  organization(login: $orgLogin) {
    name
    description
    websiteUrl @skip(if: $partialDetails)
  }
}

Note that a field (or fragment) can be followed by multiple directives. You can repeat @include multiple times or even use both @include and @skip together. All directive conditions must be met for the field (or fragment) to be included or excluded.

Neither @include nor @skip has precedence over the other. When used together, a field is included only when the include condition is true and the skip condition is false; it is excluded when either the include condition is false or the skip condition is true. The following query will never include websiteUrl no matter what value you use for $partialDetails.

Listing 3. 20. Using @include and @skip together (az.dev/gia)
query OrgInfo($orgLogin: String!, $partialDetails: Boolean!) {
  organization(login: $orgLogin) {
    name
    description
    websiteUrl @skip(if: $partialDetails) @include(if: false)
  }
}

3.4. The @deprecated directive

This special directive can be used in GraphQL servers to indicate deprecated portions of a GraphQL service’s schema, such as deprecated fields on a type or deprecated ENUM values.

When deprecating a field in a GraphQL schema, the @deprecated directive supports a reason argument to provide the reason behind the deprecation. The following is the GraphQL’s schema language representation of a type that has a deprecated field.

Listing 3. 21. The @deprecated directive
type User {
  emailAddress: String
  email: String @deprecated(reason: "Use 'emailAddress'.")
}
The @deprecated directive can only be used on the server side.

4. GraphQL fragments

When we explored directives, I kept adding "(or fragment)" whenever I mentioned the use of a directive. It is now time to discuss my favorite feature of the GraphQL language: fragments!

4.1. Why fragments?

To build anything big or complicated, the only truly helpful strategy is to split what needs to be built into smaller parts and then focus on one part at a time. Ideally, the smaller parts should be designed in a way that does not couple them with each other. They should be testable on their own, and they should also be reusable. A big system should be the result of putting these parts together and having them communicate with each other to form features.

For example, in the UI domain, the React.js library popularized the idea of using small components to build a full UI. React.js certainly was not the first library to use the concept of components, but it did make most UI developers aware of their benefits.

In GraphQL, fragments are the composition units of the language. They provide a way to split big GraphQL operations into smaller parts. A fragment in GraphQL is simply a reusable piece of any GraphQL operation.

I like to compare GraphQL fragments to UI components. Fragments, if you will, are the components of a GraphQL operation.

Splitting a big GraphQL document into smaller parts is the main advantage of GraphQL fragments. However, fragments can also be used to avoid duplicating a group of fields in a GraphQL operation. We will explore both benefits, but let’s first understand the syntax for defining and using fragments.

4.2. Defining and using fragments

To define a GraphQL fragment, you can use the fragment top-level keyword in any GraphQL document. You give the fragment a name and specify the type on which that fragment can be used. Then, you write a partial query to represent the fragment.

For example, let’s take the simple GitHub organization information query example:

query OrgInfo {
  organization(login: "jscomplete") {
    name
    description
    websiteUrl
  }
}

To make this query use a fragment for the organization fields, you first need to define the fragment.

Listing 3. 22. Defining a fragment in GraphQL
fragment orgFields on Organization {
  name
  description
  websiteUrl
}

This defines an orgFields fragment that can be used within a selection set that expands an organization object. The on Organization part of the definition in listing 3.22 is called the type condition of the fragment. Since a fragment is essentially a selection set, you can only define fragments on object types. You cannot define a fragment on a scalar value.

To use the fragment, you "spread" its name where the fields were originally used in the query.

Listing 3. 23. Using a fragment in GraphQL
query OrgInfoWithFragment {
  organization(login: "jscomplete") {
    ...orgFields
  }
}

The three dots before orgFields in listing 3.23 are what you can use to spread orgFields. The concept of spreading a fragment is similar to the concept of spreading an object in JavaScript. The same three-dots operator can be used in JavaScript to spread an object inside another object, effectively cloning that object.

The three-dotted fragment name (…​orgFields) is called a fragment spread. You can use a fragment spread anywhere you use a regular field in any GraphQL operation.

A fragment spread can only be used when the type condition of that fragment matches the type of the object in which you want to use the fragment. There are no generic fragments in GraphQL. Also, when a fragment is defined in a GraphQL document, that fragment must be used somewhere. You cannot send a GraphQL server a document that defines fragments but does not use them.

4.3. Fragments and DRY

Fragments can be used to reduce any duplicated text in a GraphQL document. Consider this example query from the GitHub API.

Listing 3. 24. Example query with repeated sections (az.dev/gia)
query MyRepos {
  viewer {
    ownedRepos: repositories(affiliations: OWNER, first: 10) {
      nodes {
        nameWithOwner
        description
        forkCount
      }
    }
    orgsRepos: repositories(affiliations: ORGANIZATION_MEMBER, first: 10) {
      nodes {
        nameWithOwner
        description
        forkCount
      }
    }
  }
}

This query uses a simple alias with field arguments to have two lists with an identical structure: one list for the repositories owned by the current authenticated user and a second list of all the repositories under organizations of which the current authenticated user is a member.

This is a simple query, but there is room for improvement. The fields under a repository connection are repeated. We can use a fragment to define these fields just once and then use that fragment in the two places where the nodes field is repeated. The nodes field is defined on the special RepositoryConnection in GitHub (it is the connection between a user and a list of repositories).

Here is the same GraphQL operation modified to use a fragment to remove the duplicated parts.

Listing 3. 25. Using fragments to minimize repetition (az.dev/gia)
query MyRepos {
  viewer {
    ownedRepos: repositories(affiliations: OWNER, first: 10) {
      ...repoInfo
    }
    orgsRepos: repositories(affiliations: ORGANIZATION_MEMBER, first: 10) {
      ...repoInfo
    }
  }
}

fragment repoInfo on RepositoryConnection {
  nodes {
    nameWithOwner
    description
    forkCount
  }
}

Pretty neat, right? But as I mentioned, the DRY benefit of fragments is less important. The big advantage of fragments is that they can be matched with other units of composition (like UI components). Let’s talk about that!

4.4. Fragments and UI components

The word component can mean different things to different people. In the UI domain, a component can be an abstract input text box or Twitter’s full 280-character tweet form with its buttons and counter display. You can pick any part of an application and call it a component. Components can be small or big. They can be functional on their own, or they can be parts that have to be put together to make something functional.

Bigger components can be composed of smaller ones. For example, Twitter’s TweetForm component may consist of a TextArea component with a TweetButton component and a few others to attach an image, add a location, and count the number of characters typed in the text area.

All HTML elements can be considered simple components. They have properties and behaviors, but they are limited because they cannot represent dynamic data. The story of UI components gets interesting when we make a component represent data. We can do that with modern libraries and frameworks like React.js, Angular.js, and Polymer.js. These data components can then be reused for any data that matches the shape they have been designed to work with. The data components do not care about what that data is; they are only concerned about the shape of that data.

The idea of rich components is comes natively to the browser with what are commonly called web components. Many browsers support most of the features needed to define and use web components. The Polymer.js project is designed to provide polyfills to support using web components in any browser and then to enhance the features offered by these components.

Let’s assume we are building an app like Twitter by using rich data components, and let’s take one example page from that app and analyze it in terms of components and their data requirements. I picked the user’s profile page for this example.

The user’s profile page is simple: it displays public information about a user, some stats, and a list of their tweets. For example, if you navigate to twitter.com/ManningBooks on Twitter, you will see something like figure 3.8.

ch03 fig 08 gqlia
Figure 3. 8. The @ManningBooks profile page at Twitter

I can see at least 13 components on this page:

  • The Header component, which can include the following components: ProfileImage, BackgroundImage, UserInfo, TweetCount, FollowingCount, and FollowersCount

  • The Sidebar component, which can include the following components: UserMedia, MediaItem, and SuggestedFollowers

  • The TweetList component, which is simply a list of Tweet components

This is just my choice of components. The page can be built with many more components, and it can also be built with just two components. No matter how small or big the components you design are, they share a simple characteristic: they all depend on data that has a certain shape.

For example, the Header component in this UI needs a data object to represent a profile. The shape of that data object might look like this.

Listing 3. 26. Possible shape of a data object for Twitter’s profile page
const profileData = {
  tweetsCount: ·-·-·,
  profileImageUrl: ·-·-·,
  backgroundImageUrl: ·-·-·,
  userName: ·-·-·,
  handle: ·-·-·,
  bio: ·-·-·,
  location: ·-·-·,
  url: ·-·-·,
  followingCount: ·-·-·,
  followersCount: ·-·-·,
};

The TweetList component needs a data object that might look like this.

Listing 3. 27. Possible shape of a data object to represent a list of tweets
const tweetList = [
  { id: ·-·-·,
    userName: ·-·-·,
    userHandle: ·-·-·,
    date: ·-·-·,
    body: ·-·-·,
    repliesCount: ·-·-·,
    tweetsCount: ·-·-·,
    likes: ·-·-·,
  },
  { id: ·-·-·,
    userName: ·-·-·,
    userHandle: ·-·-·,
    date: ·-·-·,
    body: ·-·-·,
    repliesCount: ·-·-·,
    tweetsCount: ·-·-·,
    likesCount: ·-·-·,
  },
  ·-·-·,
];

These components can be used to render information about any profile and any list of tweets. The same TweetList component can be used on Twitter’s main page, a user’s list page, or the search page.

As long as we feed these components the exact shape of data they need, they will work. This is where GraphQL comes into the picture: we can use it to describe the data shape that an application requires.

A GraphQL query can be used to describe an application data requirement. The data required by an application is the sum of the data required by that application’s individual components, and GraphQL fragments offer a way to split a big query into smaller ones. This makes a GraphQL fragment the perfect match for a component! We can use a GraphQL fragment to represent the data requirements for a single component and then put these fragments together to compose the data requirements for the entire application.

To simplify the Twitter example, we will build the profile page with just these four primary components: Header, Sidebar, TweetList, and Tweet. Let’s come up with the data required by the Twitter profile page example using a single GraphQL query for each component we identified The data required by the Header component can be declared using this GraphQL fragment.

Listing 3. 28. Fragment for the Header UI component
fragment headerData on User {
  tweetsCount
  profileImageUrl
  backgroundImageUrl
  userName
  handle
  bio
  location
  url
  createdAt
  followingCount
  followersCount
}

The data required by the Sidebar component can be declared using the following fragment.

Listing 3. 29. Fragment for the Sidebar UI component
fragment sidebarData on User {
  SuggestedFollowers {
    profileImageUrl
  }
  media {
    mediaUrl
  }
}

Note that the SuggestedFollowers part and the media part can also come from the subcomponents we identified earlier in the Sidebar component.

The data required by a single Tweet component can be declared as follows.

Listing 3. 30. Fragment for the Tweet UI component
fragment tweetData on Tweet {
  user {
    userName
    handle
  }
  createdAt
  body
  repliesCount
  retweetsCount
  likesCount
}

Finally, the data required by the TweetList component is an array of the exact data required by a single Tweet component. So, we can use the tweetData fragment here.

Listing 3. 31. Fragment for the TweetList UI component
fragment tweetListData on TweetList {
  tweets: {
    ...tweetData
  }
}

To come up with the data required by the entire page, all we need to do is put these fragments together and form one GraphQL query using fragment spreads.

Listing 3. 32. Combining fragments to form one query for a UI view
query ProfilePageData {
  user(handle: "ManningBooks") {
    ...headerData
    ...sidebarData
    ...tweetListData
  }
}

Now we can send this single ProfilePageData query to the GraphQL server and get back all the data needed for all the components on the profile page.

When the data comes back, we can identify which component requested which parts of the response and make those parts available to only the components that requested them. This helps isolate a component from any data it does not need.

But this is not the coolest thing about this approach. By making every component responsible for declaring the data it needed, these components have the power to change their data requirements when necessary without having to depend on any of their parent components in the tree.

For example, let’s assume Twitter decided to show the number of views each tweet has received next to the likesCount. All we need to do to satisfy this new data requirement is to modify the tweetData fragment.

Listing 3. 33. Modifying one fragment to match its UI component’s needs
fragment tweetData on Tweet {
  user {
    userName
    handle
  }
  createdAt
  body
  repliesCount
  retweetsCount
  likesCount
  viewsCount
}

None of the other application components need to worry about this change or even be aware of it. For example, the direct parent of a Tweet component, the TweetList component, does not have to be modified to make this change happen. That component always constructs its own data requirements by using the Tweet component’s data requirement, no matter what that Tweet component asked for. This is great. It makes maintaining and extending this app a much easier task.

Fragments are to queries what UI components are to a full application. By matching every UI component in the application to a GraphQL fragment, we give these components the power of independence. Each component can declare its own data requirement using a GraphQL fragment, and we can compose the data required by the full application by putting together these GraphQL fragments.

4.5. Inline fragments for interfaces and unions

Earlier in this chapter, we saw an example of an inline fragment when we talked about the Node interface. Inline fragments are, in a way, similar to anonymous functions that you can use without a name. They are just fragments without names, and you can spread them inline where you define them.

Here is an inline fragment use case from the GitHub API.

Listing 3. 34. Inline fragment example (az.dev/gia)
query InlineFragmentExample {
  repository(owner: "facebook", name: "graphql") {
    ref(qualifiedName: "master") {
      target {
        ... on Commit {
          message
        }
      }
    }
  }
}

Inline fragments can be used as a type condition when querying against an interface or a union. The bolded part in the query in listing 3.34 is an inline fragment on the Commit type within the target object interface; so, to understand the value of inline fragments, you first need to understand the concepts of unions and interfaces in GraphQL.

Interfaces and unions are abstract types in GraphQL. An interface defines a list of "shared" fields, and a union defines a list of possible object types. Object types in a GraphQL schema can implement an interface that guarantees that the implementing object type will have the list of fields defined by the implemented interface. Object types defined as unions guarantee that what they return will be one of the possible types of that union.

In the previous example query, the target field is an interface that represents a Git object. Since a Git object can be a commit, tag, blob, or tree, all these object types in the GitHub API implement the GitObject interface; because of that, they all get a guarantee that they implement all the fields a GitObject implements (like repository, since a Git object belongs to a single repository).

Within a repository, the GitHub API has the option to read information about a Git reference using the ref field. Every Git reference points to an object, which the GitHub API named target. Now, since that target can be one of four different object types that implement the GitObject interface, within a target field, you can expand the selection set with the interface fields; but you can also conditionally expand its selection set based on the type of that object. If the object that this ref points to happens to be a Commit, what information from that commit are you interested in? What if that object is a Tag?

This is where inline fragments are useful because they basically represent a type condition. The inline fragment in the previous query essentially means this exact condition: if the object pointed to by the reference is a commit, then return the message of that commit. Otherwise, the target will return nothing. You can use another inline fragment to add more cases for the condition.

The union concept is probably a bit easier to understand. It is basically OR logic. A type can be this or that. In fact, some union types are named xOrY. In the GitHub API, you can see an example under a repository field, where you can ask for issueOrPullRequest. Within this union type, the only field you can ask for is the special __typename, which can be used to answer the question, "Is this an issue or a pull request?"

Here is an example from the facebook/graphql repository.

Listing 3. 35. Example GraphQL union type (az.dev/gia)
query RepoUnionExample {
  repository(owner: "facebook", name: "graphql") {
    issueOrPullRequest(number: 3) {
      __typename
    }
  }
}

The issueOrPullRequest with number 3 on this repository happens to be an issue. If you try the query with number 5 instead of 3, you should see a pull request. An inline fragment is useful here to conditionally pick fields within an issueOrPullRequest based on the type. For example, maybe we are interested in the merge information of a pull request and the closing information of an issue. Here is a query to pick these different fields based on the type of the issueOrPullRequest whose number is 5.

Listing 3. 36. Using inline fragments with union types (az.dev/gia)
query RepoUnionExampleFull {
  repository(owner: "facebook", name: "graphql") {
    issueOrPullRequest(number: 5) {
      ... on PullRequest {
        merged
        mergedAt
      }
      ... on Issue {
        closed
        closedAt
      }
    }
  }
}

Since #5 (in this repository) is a pull request, the merged and mergedAt fields will be used for this query.

Another common use of union types is to implement a search field to search among multiple types. For example, a GitHub user search might return a user object or an organization object. Here is a query to search GitHub users for the term "graphql".

Listing 3. 37. The union-type search field (az.dev/gia)
query TestSearch {
  search(first: 100, query: "graphql", type: USER) {
    nodes {
      ... on User {
        name
        bio
      }
      ... on Organization {
        login
        description
      }
    }
  }
}

You should see users who have the term "graphql" somewhere in their profile and organizations with that term in their name or description. When the matching returned item is a User object, the fields name and bio are returned; and when the item is an Organization object, the fields login and description are returned.

5. Summary

  • You can pass arguments to GraphQL fields when sending requests. GraphQL servers can use these arguments to support features like identifying a single record, limiting the number of records returned by a list field, ordering records and paginating through them, searching and filtering, and providing input values for mutations.

  • You can give any GraphQL field an alias name. This enables you to customize a server response using the client’s request text.

  • You can use GraphQL directives to customize the structure of a GraphQL server response based on conditions in your applications.

  • Directives and field arguments are often used with request variables. These variables make your GraphQL requests reusable with dynamic values without needing to resort to string concatenation.

  • You can use fragments, which are the composition units of GraphQL, to reuse common parts of a GraphQL query and compose a full query by putting together multiple fragments. This is a winning strategy when paired with UI components and their data needs. GraphQL also supports inline fragments that can be used to conditionally pick information out of union object types or object types that implement an interface.