Graphql In Action
Customizing Organizing Operatoins

Customizing and Organizing GraphQL Operations

This chapter covers

  • Using arguments to customize what a field in a GraphQL request returns

  • Using aliases to customize the property names in a response object

  • Using directives to describe alternate runtime executions

  • Using fragments to reduce any duplicated text in a GraphQL document

  • Composing queries and separating data requirement responsibilities

  • Using inline fragments with interfaces and unions

In any non-trivial application, you will have to do many things beyond asking the server a direct and 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 needs. Sometimes, you need to remove parts of the data or go back to the server and ask for other parts that are needed 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 in your requests. Luckily, the GraphQL language offers many built-in features that will allow you to do all of the above 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 will have access to these values and can use them to customize the response they return for that field.

Let’s look at some use cases for these field arguments and examples of these cases as used by the GitHub GraphQL API (developer.github.com).

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 are asking an API for information on a single user, you would usually send along with your request the ID for the user you are interested in. You can also send their email address, username, or their Facebook-id connection if, for example, you are logging them in through a Facebook button.

Here is an example query that could be asking for information about the user whose email address is "[email protected]":

Listing 3. 1. Query example for 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 that represents a single record, the argument value that you pass to identify that record has to be a unique value on that field record in the database. For example, you cannot pass the full name of the person to identify their user record because the database might have 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 lookup a single person record.

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

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

See the section about GraphQL Fragments later in this chapter for more details about the inline-fragment you see in the example of listing 3.2.

In the GitHub API, some examples of single-record lookup fields are user, repository, project, organization, and many more. Here is an example to read information about the Facebook organization, which is the organization that hosts the official GraphQL specification repository.

Listing 3. 3. Query for one organization information | az.dev/gia
query OrgInfo {
  organization(login: "facebook") {
    name
    description
    websiteUrl
  }
}

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

When you ask any API 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 enable listing records in a collection without a limit. You do not want to have a way for a client to fetch more than a few hundred records at a time because that would put your API server under the risk of resource exhaustion and will not scale very 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 Facebook organization. You can use the repositories field within the organization field in the OrgInfo query of listing 3.3. You should notice how GitHub will ask for a first or last value:

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

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

Here is the query that you can use to retrieve the first 10 repositories under the Facebook organization:

Listing 3. 4. Query for first 10 repositories under organization | az.dev/gia
query First10Repos {
  organization(login: "facebook") {
    name
    description
    websiteUrl
    repositories(first: 10) {
      nodes {
        name
      }
    }
  }
}

By default, the GitHub API will order the repositories by their date of creation in an ascending order. 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 Facebook 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. Query for first 10 alphabetically-ordered repositories under organization | az.dev/gia
query orgReposByName {
  organization(login: "facebook") {
    repositories(first:10, orderBy:{field: NAME, direction: ASC}) {
      nodes {
        name
      }
    }
  }
}

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

Here is one query you can use for that:

Listing 3. 6. Query for top-10 most popular repositories under organization | az.dev/gia
query OrgPopularRepos {
  organization(login: "facebook") {
    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 or before to offset the results returned by the arguments first or last.

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 is using 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 that we can use with the before and after arguments.

To have a way 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 "edge". The edges field can represent a list of paginated records and every object in that list holds a unique cursor value.

Here is the stargazers query in the previous example modified to include cursor values through the edges field:

Listing 3. 7. Query example for working with cursors under edges | az.dev/gia
query OrgRepoConnectionExample {
  organization(login: "facebook") {
    repositories(first:10, orderBy:{field: STARGAZERS, direction: DESC}) {
      edges {
        cursor
        node {
          name
        }
      }
    }
  }
}

Note how within an edges field we are now asking 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 these values as the after or before arguments to fetch extra pages of data. For example, to fetch the second page of the most popular repositories under the Facebook organization, you need to identify the cursor of the last popular repository you see in the first page (which was "fresco" when I tested these queries) and use that cursor value as the after value:

Listing 3. 8. Query example to fetch second page of popular repositories | az.dev/gia
query OrgPopularReposPage2 {
  organization(login: "facebook") {
    repositories(
      first: 10,
      after: "Y3Vyc29yOnYyOpLNOE7OAeErrQ==",
      orderBy: {field: STARGAZERS, direction: DESC}
    ) {
      edges {
        cursor
        node {
          name
        }
      }
    }
  }
}

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

Listing 3. 9. Query example for meta pagination information | az.dev/gia
query OrgReposMetaInfoExample {
  organization(login: "facebook") {
    repositories(
      first: 10,
      after: "Y3Vyc29yOnYyOpLNOE7OAeErrQ==",
      orderBy: {field: STARGAZERS, direction: DESC}
    ) {
      totalCount
      pageInfo {
        hasNextPage
      }
      edges {
        cursor
        node {
          name
        }
      }
    }
  }
}

Since the Facebook organization has a lot more than 20 repositories (2 pages in this example), that hasNextPage field will be true to indicate that. When you fetch the very last page, the hasNextPage will turn false indicating that there is no more data for you to fetch. This is much better than having to do an extra empty page fetch to conclude that we 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. Query example for using field arguments to search | az.dev/gia
query SearchExample {
  repository(owner: "twbs", name: "bootstrap") {
    projects(search: "v4.1", first: 10) {
      nodes {
        name
      }
    }
  }
}

Note how 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 your GitHub viewer field, you can see a list of all your repositories. By default, that list will include 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. Query example for 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 repository under the Facebook organization:

Listing 3. 12. Example for using arguments to provide input values to a mutation | az.dev/gia
mutation StarARepo {
  addStar(input: {starrableId: "MDEwOlJlcG9zaXRvcnkzODM0MjIyMQ=="}) {
    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 the GitHub API mutations use this single required input field argument that represent 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 a lot more cases where a field argument can be useful. Explore the GitHub API and other publicly-available GraphQL APIs to look for more useful patterns for field arguments.

2. Renaming Fields with Aliases

The alias feature in a GraphQL operation is a very simple but powerful feature because it allows you to customize a response coming from the server through the request itself. This means 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 the values in some UI template. However, 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 now? Assume that changing the application UI code itself is not an option.

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

Luckily, in GraphQL we have this awesome alias feature that allows us to declaratively instruct the API server to return fields using different names. All you need to do is to specify an alias for that field, which you can do using the 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 alias | az.dev/gia
query ProfileInfoWithAlias {
  user(login: "samerbuna") {
    name
    companyName: company
    bio
  }
}

This will give you the exact 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 that you need on a server response goes beyond simple renaming of fields. Sometimes, you 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 because you can use directives to conditionally include or exclude the whole field. Also, besides fields, directives can be used with fragments and the top-level operations themselves.

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 will have more directives. You can see the list of directives supported by a schema using this introspective query:

Listing 3. 15. Query for all supported directives | az.dev/gia
query AllDirectives {
  __schema {
    directives {
      name
      description
      locations
      args {
        name
        description
        defaultValue
      }
    }
  }
}

This query will show the names and description for each directive and it will also include an array of all possible locations where that directive can be used. In addition, it will also include a list of all arguments supported by that directive. Each directive can optionally receive a list of arguments, and just like field arguments some argument values might 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 all directives supported by a schema

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

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 of them.

3.1. Variables and input value

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 in GraphQL operations to make these operations generically reusable and avoid hard coding values and having to do string concatenation.

To use a variable in a GraphQL operation, you need to define its type first. You do that by providing arguments to any named operation. For example, let’s take the organization query example that we used to read information about the Facebook 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).

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

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.

We can use the same syntax to define the new variable now, 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 how in 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 would be:

{
  "orgLogin": "facebook"
}

Now you can execute the query with different JSON objects, making it reusable for different organizations.

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

A variable like the $orgLogin can also have a default value, in which case it would 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 "facebook" as the default value of $orgLogin using this syntax:

Listing 3. 17. Using default values for variables | az.dev/gia
query OrgInfoWithDefault($orgLogin: String = "facebook") {
  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 that has 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 variable 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 means to include the field when the query is executed with $someTest set to true and do not 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 you want to conditionally include an organization’s websiteUrl based on whether you 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!) {

What we want to accomplish now is to only include the websiteUrl 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 itself. Here is the full query:

Listing 3. 18. Example for using 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 and see how the response will now honor that Boolean value and use it to include or exclude the websiteUrl from the response object.

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 is useful to avoid manually 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 decided to name it $partialDetails. Instead of manually 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:

Listing 3. 19. Example for using 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 will be included only when both the include condition is true and the skip condition is false, and it will be 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 is a special directive that 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 schema language representation of a type that has a deprecated field:

Listing 3. 21. Example for using the @deprecated directive
type UserType {
  emailAddress: String
  email: Stirng @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 an "(or fragment)" whenever I mentioned the use of that directive. It is now time to explore my favorite feature of the GraphQL language. Fragments!

4.1. Why fragments?

To build anything big or complicated, the one and 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 User Interface (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 components 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 on what type 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: "facebook") {
    name
    description
    websiteUrl
  }
}

To make this query use a fragment for the fields of the organization, 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: "facebook") {
    ...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 just defines fragments and 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 for a 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 of identical structure, one list for the repositories owned by the current authenticated user and the other 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 repetitions in GraphQL operations | 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 the less important one. The big advantage of fragments is the fact that they can be matched to 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 that a component. They can be small or big. They can be functional on their own or they can just be parts that have to be put together to make something functional.

Bigger components can be composed from smaller ones. For example, the Twitter’s TweetForm component can be composed from a TextArea component with a TweetButton component and a few other 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 in the fact that 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 really care about what that data is. They are only concerned about the shape of that data.

The idea of rich components is actually coming natively to the browser with what is commonly labeled as Web Components. Many browsers already support most of the features needed to define and use Web Components. The Polymer.js project is designed to first provide polyfills to support using Web Components in any browser and then enhance their features.

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 a simple page that displays public information about a user, some stats, and the list of their tweets. For example, if you navigate to twitter.com/ManningBooks on Twitter, you will see something like this:

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

I can see at least 15 components on this page:

  • The Header component, which could include the following list of components: ProfileImage, BackgroundImage, TweetCount, FollowingCount, FollowersCount, LikesCount, and ListsCount.

  • The Sidebar component, which could include the following list of components: UserInfo, FollowersYouKnow, UserMedia, and MediaItem.

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

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

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 to be used in the Twitter’s profile page
const profileData = {
  profileImageUrl: ...,
  backgroundImageUrl: ...,
  tweetsCount: ...,
  followingCount: ...,
  followersCount: ...,
  likesCount: ...,
  listsCount: ...,
};

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 just work. This is where GraphQL comes into the picture because we can use it to describe the shape of data needed by an application.

To simplify the Twitter example, I will consider that we are going to build the profile page with just these main four components: Header, Sidebar, TweetList, and Tweet.

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 simply 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 whole application.

Let’s come up with the data required by the Twitter’s 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 {
  profileImageUrl
  backgroundImageUrl
  tweetsCount
  followingCount
  followersCount
  likesCount
  listsCount
}

The data required by the Sidebar component can be declared using:

Listing 3. 29. Fragment for the Sidebar UI component
fragment sidebarData on User {
  userName
  handle
  bio
  location
  url
  createdAt
  followersYouKnow {
    profileImageUrl
  }
  media: {
    mediaUrl
  }
}

Note that the followersYouKnow part and the media part can also come from the sub-components we identified earlier in the Sidebar component.

The data required by a single Tweet component can be declared using:

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 whole 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 that 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 will 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 components in the application 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 need 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 simply 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 just putting these GraphQL fragments together.

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 will need to first understand the concepts of unions and interfaces in GraphQL.

Interfaces and unions are abstract types in GraphQL. An interface defines a list of fields and a union defines a list of possible object types. Object types in a GraphQL schema can implement an interface, which forms a guarantee 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 and, 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 out of 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 an OR logic. A type can be this or that. In fact, some union types are named as xOrY. In the GitHub API, you can see an example of that 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 for a 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, so 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. Query a 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 who have that term in their name or description. When the matching returned item is a User object, the fields name and bio will be returned and when the item is an Organization object, the fields login and description will be returned.

5. Summary

GraphQL has a few features that you can use to organize client requests and customize server responses:

  • 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.