Fundamentals of GraphQL
This article is part 2 of the series on Exploring GraphQL. Check out the other articles from the series:
- Part 1: What is GraphQL and why Facebook felt the need to build it?
- Part 3: Building a GraphQL Server using NodeJS and Express
- Part 4: How to implement Pagination and Mutation in GraphQL
- Part 5: Introducing the Apollo GraphQL Platform for implementing the GraphQL Specification
- Part 6: How to Connect MongoDB to a GraphQL Server?
- Part 7: GraphQL Subscriptions - Core Concepts
- Part 8: Implementing GraphQL Subscriptions using PubSub
This article is part 2 of the series on Exploring GraphQL. In this article, we are going to learn the fundamental concepts used in writing GraphQL queries. We will learn how to fetch particular records by passing arguments in a query at the root and nested levels, how to fetch multiple instances of the same field but with different arguments using Alias and how to write reusable units of a query using Fragments. We will also learn how variables can be used to write dynamic queries and how Directives are used in creating a result-set based on some condition. We will also explore one more object type called Mutation
later in the article.
Highlights from the Part I of the series
- Representational State Transfer (REST) is an architectural style intended to share large amounts of data between multiple parties. The systems that use REST are called Restful systems. They are used for transferring resources between web services. REST is a simple and flexible model for sharing resources across web services. However, there are some down-sides in this technique that causes serious performance issues in low-bandwidth mobile devices.
- Facebook built GraphQL as a replacement for REST. GraphQL is a static strong-typed query language that let clients declaratively specify their data requirements. The clients specify the shape of the data that they need and the server responds with the exact same shape as the response.
- GraphQL solves some of the issues such as multiple endpoints, under-fetching, and over-fetching of resources and versioning for applications experienced while using the REST style architecture.
- GraphQL uses a Type System specification for writing queries. It helps in defining the structure of the schema of a query. We can use the scalar types -
Int
,Float
,String
,Boolean
andID
, enumeration and object types in the GraphQL schema. - We also explored GraphiQL, which is an interactive in-browser IDE.
You can read more on the above points here
GraphQL in GitHub APIs
GitHub made an announcement in Sept 2016 regarding the use of GraphQL in its public APIs. The GitHub REST APIs were responsible for handling 60% of the requests made to their servers. The REST APIs were not scalable and started impacting the performance as the applications became more complex.
Here's an excerpt from the GitHub blog announcement -
Our responses were bloated and filled with all sorts of
*_url
hints in the JSON responses to help people continue to navigate through the API to get what they needed. Despite all the information we provided, we heard from integrators that our REST API also wasn’t very flexible. It sometimes required two or three separate calls to assemble a complete view of a resource. It seemed like our responses simultaneously sent too much data and didn’t include data that consumers needed. - The GitHub Blog
GitHub also faced problems while collecting meta information about the endpoints.
We wanted to collect some meta-information about our endpoints. For example, we wanted to identify the OAuth scopes required for each endpoint. We wanted to be smarter about how our resources were paginated. We wanted assurances of type-safety for user-supplied parameters. We wanted to generate documentation from our code. - The GitHub Blog
The above two issues clearly explain the rationale behind the switch from REST to GraphQL. With GraphQL, GitHub is now able to design its servers to send only those fields that are requested by the client. This helps in improving the performance of the mobile app clients where smaller payloads are required. GitHub has done an amazing job on building a GitHub GraphQL API Explorer with the help of GraphiQL. It includes the documentation about various types, fields and the interfaces used in GraphQL APIs.
You can use this explorer to run complicated queries on your GitHub account. We would be using this explorer in the rest of the article to understand the fundamental concepts of GraphQL.
Arguments in GraphQL query
Let's write a query to get the name
, createdAt
date and the diskUsage
of the first 5 repositories of the logged-in user
graphqlquery { viewer { name, email, repositories(first:5) { nodes { name, createdAt, diskUsage } } } }
The root-level query
is one of the object types. It is used for specifying the nested selection set. As we can see from the documentation, viewer
is a field of type User
. It has information about the currently authenticated user. It defines a selection set to get the name
, email
and the repositories
of the user. Notice the use of first:5
alongside the repositories
field. This is how we pass arguments in the GraphQL query. first:5
tells the server to send the first 5 repositories of the user. repositories
is again an object type and the client is requesting for fields name
, createdAt
and diskUsage
.
When you run this query in the GitHub GraphiQL, you'll get the response in the format as below
graphql{ "data": { "viewer": { "name": "Ankita Masand", "email": "amasand23@gmail.com", "repositories": { "nodes": [ { "name": "Inventory-React", "createdAt": "2016-10-07T11:00:07Z", "diskUsage": 2 }, { "name": "chat", "createdAt": "2016-10-23T05:51:25Z", "diskUsage": 15 }, { "name": "simple-calculator", "createdAt": "2016-10-31T04:20:29Z", "diskUsage": 3 }, { "name": "Book-Store", "createdAt": "2016-11-01T13:24:22Z", "diskUsage": 3 }, { "name": "pollapp", "createdAt": "2017-01-18T05:47:38Z", "diskUsage": 966 } ] } } } }
Please note: the response may vary as per the logged-in user.
The arguments can be passed in the GraphQL query using parenthesis ()
in the object field. In REST, we could only pass a set of arguments along with the URL of the endpoint. But with GraphQL, we can pass arguments even for the nested fields. This helps in writing intuitive queries on the front-end that clearly states what is required from the server. The arguments help in getting a specific set of data from the server and it can also be used for pagination. The arguments can be one of the scalar types, object types, and enum.
Let's write a query that accepts a scalar, an object and, an enum as arguments
graphqlquery { securityVulnerabilities ( first: 5, orderBy: { field: UPDATED_AT, direction: DESC }, severities: HIGH ) { nodes { package { name } } } }
The above query is used for fetching the name of the packages that have a high-security vulnerability. securityVulnerabilities
is an object type field. We are passing three arguments to this field - first
is of type Int
, orderBy
is of type object
and it includes the field
and the direction
to filter the vulnerabilities. field
and direction
are enums and UPDATED_AT
and DESC
are one of the values in these enums. The severities
is also an enum with values as LOW
, MODERATE
, HIGH
and CRITICAL
.
Alias in GraphQL
Alias is generally used for giving another name to an entity. As you might have noticed, the query
sent by the client is of type object
.We can specify a field only once as fields are unique keys in objects.
We can fetch multiple instances of a field in GraphQL using Alias as below
graphqlquery { viewer { small: avatarUrl(size: 30), medium: avatarUrl(size: 40), large: avatarUrl(size: 50) } }
The above query gets the URL of the image of the user in three different dimensions. The size
argument passed to the avatarUrl
is of type Int
and it is used for specifying the size of the edge of a square image. small
, medium
and large
are the aliases of the same field avatarUrl
.
Here's the response of the above query
graphql{ "data": { "viewer": { "small": "https://avatars0.githubusercontent.com/u/20657538?s=30&v=4", "medium": "https://avatars0.githubusercontent.com/u/20657538?s=40&v=4", "large": "https://avatars1.githubusercontent.com/u/20657538?s=50&v=4" } } }
Please note: the response may vary as per the logged-in user.
The server sends back the response using the same keys for the alias as used in the query on the client-side.
Fragments in GraphQL
Fragments are used for defining the reusable parts of a query. Let's say, we have to display the list of forked repositories and the ones created by the user (repositories that are not forked).
They query for dealing with this use-case can be written as
graphqlquery { viewer { forked: repositories ( isFork: true, first: 5, orderBy: { field: UPDATED_AT, direction: DESC } ) { nodes { createdAt, name, diskUsage, forkCount } }, unforked: repositories ( isFork: false, first: 5, orderBy: { field: UPDATED_AT, direction: DESC } ) { nodes { createdAt, name, diskUsage, forkCount } } } }
The fields forked
and unforked
are the aliases of the repositories
field. Please note the value of isFork
filter in both of these fields as true
and false
respectively. The other filters - first
is used for getting the first 5 repositories and orderBy
is used for getting the repositories in the descending order of the last updated date. We want the same fields createdAt
, name
, diskUsage
and forkCount
for both of them. We are repeating the same code again. We can solve this problem using Fragments. Fragments lets us write a reusable unit of a query. Let's see how this can be achieved using Fragments
graphqlfragment respositoryInfo on Repository { createdAt, name, diskUsage, forkCount } query { viewer { forked: repositories ( isFork: true, first: 5, orderBy: { field: UPDATED_AT, direction: DESC } ) { nodes { ...respositoryInfo } }, unforked: repositories ( isFork: false, first: 5, orderBy: { field: UPDATED_AT, direction: DESC } ) { nodes { ...respositoryInfo } } } }
In the above query, we have defined a fragment respositoryInfo
on type Repository
. The respositoryInfo
fragment is included in the query using the spread operator.
Here's the response of the above query
graphql{ "data": { "viewer": { "forked": { "nodes": [ { "createdAt": "2018-11-15T02:51:16Z", "name": "deep-learning-v2-pytorch", "diskUsage": 129351, "forkCount": 0 }, { "createdAt": "2018-10-09T16:48:15Z", "name": "react-jsonschema-form", "diskUsage": 4956, "forkCount": 0 }, { "createdAt": "2018-10-09T06:07:10Z", "name": "webpack.js.org", "diskUsage": 321869, "forkCount": 0 }, { "createdAt": "2017-10-28T08:20:40Z", "name": "guides", "diskUsage": 29071, "forkCount": 0 } ] }, "unforked": { "nodes": [ { "createdAt": "2019-04-04T04:16:35Z", "name": "ankitamasand.github.io", "diskUsage": 1, "forkCount": 0 }, { "createdAt": "2018-12-31T15:24:10Z", "name": "bookskeep-pwa", "diskUsage": 716, "forkCount": 4 }, { "createdAt": "2019-01-27T16:23:41Z", "name": "nlp-chatbot", "diskUsage": 11, "forkCount": 0 }, { "createdAt": "2019-01-23T12:47:53Z", "name": "virtual-stock-ml", "diskUsage": 0, "forkCount": 0 }, { "createdAt": "2018-12-22T06:08:46Z", "name": "react-chrome-ext-boilerplate", "diskUsage": 192, "forkCount": 0 } ] } } } }
We can also include additional fields in the query apart from the ones that are already included in the fragment respositoryInfo
.
Fragments are pretty helpful in writing queries that include repetitive units. The complicated queries can be split into smaller chunks using fragments.
Variables in GraphQL
The values of the arguments in the above queries are hard-coded and this is not a real-world scenario. In the actual implementation, the users can select different values for these arguments on the user-interface. For example, a user may want to order the list of repositories in the above query in ascending order. One of the ways of implementing this use-case is to concatenate static and dynamic parts of the string to form a query. However, this is not a cleaner way to do things and is also not recommended in the GraphQL documentation.
We can use variables to pass dynamic parameters as
graphqlquery ($first: Int, $orderBy: RepositoryOrder){ viewer { repositories (first: $first, orderBy: $orderBy) { nodes { name } } } }
The above query accepts the variables $first
of type Int
and $orderBy
of type RepositoryOrder
. Now the values of first
and orderBy
in the repositories selection set is dynamically passed by the client. Notice the use of $
along with the name of the variable. The type of variables should also be included in the arguments. You should know the type if you are using variables of complex object types. For example, in the above query, the orderBy
field is of type RepositoryOrder
. However, it is optional to define the type of variables in the arguments. But it is a good practice to always the type for variables.
The variables can be sent in the JSON format as
graphql{ "first": 5, "orderBy": { "field": "UPDATED_AT", "direction": "DESC" } }
How to define non-null values for variables in GraphQL
If the type of the variable orderBy
was not specified in the above query, we could have even sent null
as the value for orderBy
. If an argument is mandatory and should have non-null value, it should be specified using the exclamation mark !
as below
graphqlquery ($first: Int!, $orderBy: RepositoryOrder!){ viewer { repositories (first: $first, orderBy: $orderBy) { nodes { name } } } }
Please note the use of !
in the above query.
How to define default values for variables in GraphQL
We can define default values for variables in GraphQL query as below
graphqlquery ($first: Int = 5, $orderBy: RepositoryOrder){ viewer { repositories (first: $first, orderBy: $orderBy) { nodes { name } } } }
Notice how 5
is specified as the value for the $first
variable. If the value of first
variable is not defined in the variables object, the server would assume its value as 5
.
Directives in GraphQL
Directives are used for changing the structure of a query based on a condition. We can include some of the fields in a query only if a particular condition is met. Let's say, we would want to include the list of followers in our query only when its corresponding client-side variable is set to true
.
Let's see this in action
graphqlquery ($showFollowers: Boolean!){ viewer { name, email followers (first: 10) @include(if: $showFollowers) { nodes { name, email } } } }
Here's the variables JSON object
json{ "showFollowers": true }
The above query is in control of the client! It sends the list of followers only when the value of the variable showFollowers
is set to true
. @include (if: Boolean)
is a directive. It is used when fields are to be included in the result based on a condition. Currently, GraphQL supports two directives and they are listed below
@include (if: Boolean)
- Used for including fields in the query result based on the value of its arguments
@skip (if: Boolean)
- Used to skip a field when a condition passed to it as an argument is true
Directives are a powerful feature for writing GraphQL queries. We can also write custom directives on the server-side.
Root Object types in GraphQL - Query and Mutation
As you see in the documentation of GitHub GraphQL APIs, there are two root types - Query
and Mutation
. Until now, we have been writing queries of root type Query
. The type Query
is generally used for fetching data from the server while the type Mutation
is used for causing side-effects on the server. Mutations can be used for writing data on the server. For example, we can use mutation for creating, updating or deleting an object on the server.
The types Query
and Mutation
are objects and we can define a set of selection fields inside each of them. Like in Query, we can define multiple fields in Mutation. The fields defined in mutation will be executed in series, unlike query where fields are executed in parallel.
These types can be defined as
graphql// query query { // ... } // mutation mutation { // ... }
The use of query
keyword while defining a query is optional. But it is recommended to always define root types for queries and mutations.
Let's check out some of the mutation types in GitHub explorer. Head over to the root type Mutation
. You will see fields like addTopicSuggestion
, addComment
etc. These fields take some payload as the input and makes the required modifications to the data. For example, the addComment
field is of type AddCommentPayload
. It takes an input object of type AddCommentInput
. The AddCommentInput
object includes scalar fields - subjectId
, body
and clientMutationId
. The mutation adds a comment on the supplied subject Id. It sends back the object of type AddCommentPayload
that has the updated comments information.
We will learn how to write mutations in the next articles.
Conclusion
In this tutorial, we learned the basic concepts of GraphQL. Let's recap all that we have learned so far
- We can pass arguments in the GraphQL query at the root as well as the nested levels. The arguments are used for getting a specific set of data from the server. They can be of scalar types, object types or enums.
- We can fetch multiple instances of the same field using an alias. Alias is used for defining different parameters for the same instance of a field.
- Fragments are used for writing reusable units of a query. They can be included in a query using the spread operator
- We can pass dynamic values for arguments in a query using variables. The variables can be passed to the server as a separate JSON object. We can also specify the type of variable for strict type-checking.
- Directives can be used for adding or removing fields from the query result based on a condition. GraphQL supports
if
andskip
directives for checking the values of the arguments passed to the field. - The types
Query
andMutation
are defined at the root level. Mutations are used for causing side-effects to the server.
Now that you have understood all the fundamental concepts in GraphQL, we are ready to build our own GraphQL server. In the next article, we will be building a GraphQL server using Express and learn how to write resolvers
to get the values of the fields in a query.
Next in series: Building a GraphQL Server using NodeJS and Express