GraphQL

Nikola Jovanovic

2021-03-16

Introduction

GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data.

The official documentation definition of GraphQL might seem confusing at first, but allow me to break it down for you. GraphQL provides a query language that simplifies the methods used for accessing data from an API cleanly and predictably. Using the query language, you can define exactly what data you need from the server without over-fetching (downloading data you won’t use). When the server receives the query, it is first checked to ensure it only refers to the types and fields defined in a server-side GraphQL service. A service is basically a type system in which the server's data is clearly defined and categorized. After the checking process is complete, the server executes the provided query through the service and returns the requested data. The client-server interaction might look something like this:

Client request:

1query {
2  authors {
3    id
4    name
5    books(first: 2) {
6      name
7    }
8  }
9}

Server response:

1{
2  "data": {
3    "authors": [
4      {
5        "id": 1,
6        "name": "Stephen King",
7        "books": [
8          {
9            "name": "The Shining"
10          },
11          {
12            "name": "It"
13          }
14        ]
15      },
16      {
17        "id": 2,
18        "name": "Agatha Christie",
19        "books": [
20          {
21            "name": "The A.B.C. Murders"
22          },
23          {
24            "name": "Murder on the Orient Express"
25          }
26        ]
27      }
28    ]
29  }
30}

Setup

To start using GraphQL, you first need to set up a GraphQL server. Many programming languages support GraphQL, and creating a server is possible in almost all back-end languages. A list of available languages, along with a short setup tutorial for each of them, is available in the official documentation: https://graphql.org/code/.

In our example, we will be creating a server using Express.

Creating a server

The first two things we need to do are initialize npm and install the required packages

1npm init
2npm install graphql express express-graphql --save

Now we set up a basic Express server:

1const express = require('express')
2
3const app = express()
4app.listen(5000, () => console.log('Server Running'))

Next, we will add GraphQL to our server:

1const express = require('express')
2const expressGraphQL = require('express-graphql')
3
4const app = express()
5
6app.use('/graphql', expressGraphQL({
7  graphiql: true
8}))
9
10app.listen(5000, () => console.log('Server Running'))

Here we told the server that the GraphQL endpoint will run on localhost:5000/graphql URL. And we also initialized GraphiQL, a user interface that will easily simulate the client-server interaction.

Basic schemas

At this point, the server is running, and navigating to localhost:5000/graphql will give us an error saying that our GraphQL server needs to have a schema. GraphQL uses schemas to define the structure of the data stored on the server (or, more commonly, in a database). The query that is received from the client is matched against the schema, and only the data that is contained within the query is returned to the client.

Defining a schema in our server looks like so:

1const express = require('express')
2const expressGraphQL = require('express-graphql')
3const {
4  GraphQLSchema,
5  GraphQLObjectType,
6  GraphQLString
7} = require('graphql')
8
9const app = express()
10
11const RootQueryType = new GraphQLObjectType({
12  name: 'Query',
13  fields: () => ({
14    message: {
15      type: GraphQLString,
16      description: 'Boring string example',
17      resolve: () => 'Hello World'
18    }
19  })
20})
21
22const schema = new GraphQLSchema({
23  query: RootQueryType
24})
25
26app.use('/graphql', expressGraphQL({
27  schema: schema,
28  graphiql: true
29}))
30
31app.listen(5000, () => console.log('Server Running'))

First, we import the GraphQLSchema object from the GraphQL library we previously installed. The GraphQLObjectType object is used for creating statically typed objects called types, which can contain other types within them (like GraphQLString), and they are what defines the structure of the data that we provide to the client from the database. This will become easier to understand as we go.

At this point, if you refresh localhost:5000/graphql, you should see the GraphiQL interface. The left part represents the client-side request, while the right part represents the server-side response. Feel free to explore and understand how it works. If you type in { message } as a query, you should see something like the following as a response:

In the previous example, RootQueryType represents the simplest type of object that only returns a Hello World string without any other functionality. The schema represents an object with different operations, pointing to their respective types. For now, only the query operation is defined, which is used to fetch data from the server, similar to how the GET request works. When sending the query from the client-side, we don’t have to declare its type if we want it to be a query because it is the default operation type. There are other operation types, like mutation, which is used to change data on the server. The query operation points to RootQueryType, which, as previously stated, resolves the request by returning a Hello World string.

Data-driven schemas

We can make RootQueryType more dynamic by having it point to some external data. You can import the following data file however you like, or you can put it directly into the main server file. In a real-life scenario, this file would be replaced by the data from a database.

Note: From now on only a new code will be shown to avoid having huge walls of code, and of course, repetition.

1const data = {
2  authors: [
3    {
4      id: 1,
5      name: "Stephen King",
6    },
7    {
8      id: 2,
9      name: "Agatha Christie",
10    }
11  ],
12  books: [
13    {
14      id: 1,
15      authorID: 1,
16      year: 1977,
17      name: "The Shining"
18    },
19    {
20      id: 2,
21      authorID: 1,
22      year: 1986,
23      name: "It"
24    },
25    {
26      id: 3,
27      authorID: 1,
28      year: 1983,
29      name: "Pet Sematary"
30    },
31    {
32      id: 4,
33      authorID: 1,
34      year: 1987,
35      name: "Misery"
36    },
37    {
38      id: 5,
39      authorID: 2,
40      year: 1936,
41      name: "The A.B.C. Murders"
42    },
43    {
44      id: 6,
45      authorID: 2,
46      year: 1934,
47      name: "Murder on the Orient Express"
48    },
49    {
50      id: 7,
51      authorID: 2,
52      year: 1939,
53      name: "And Then There Were None"
54    },
55    {
56      id: 8,
57      authorID: 2,
58      year: 1967,
59      name: "Endless Night"
60    }
61  ]
62}

To receive data from this file, we first need to define a book type and an author type:

1const BookType = new GraphQLObjectType({
2  name: "Book",
3  description: "A single book",
4  fields: () => ({
5    id: { type: GraphQLInt },
6    authorId: { type: GraphQLInt },
7    name: { type: GraphQLString },
8    year: { type: GraphQLInt }
9  })
10})
11
12const AuthorType = new GraphQLObjectType({
13  name: "Author",
14  description: "A single author",
15  fields: () => ({
16    id: { type: GraphQLInt },
17    name: { type: GraphQLString }
18  })
19})

We also need to create fields that point to these types in the RootQueryType:

1const RootQueryType = new GraphQLObjectType({
2  name: 'Query',
3  fields: () => ({
4    authors: {
5      type: new GraphQLList(AuthorType),
6      description: 'A list of all authors',
7      resolve: () => authors
8    },
9    books: {
10      type: new GraphQLList(BookType),
11      description: 'A list of all books',
12      resolve: () => books
13    }
14  })
15})

Here we used some new GraphQL object types like GraphQLList and GraphQLInt, so we need to import them from the GraphQL library:

1const {
2  GraphQLSchema,
3  GraphQLObjectType,
4  GraphQLString,
5  GraphQLList,
6  GraphQLInt
7} = require('graphql')

Now our data is ready to be accessed by client requests. If, for example, we would like to get all the book names from all the authors, all we would have to do is send the following query:

1{
2  books {
3    name
4  }
5}
1{
2  "data": {
3    "books": [
4      {
5        "name": "The Shining"
6      },
7      {
8        "name": "It"
9      },
10      {
11        "name": "Pet Sematary"
12      },
13      {
14        "name": "Misery"
15      },
16      {
17        "name": "The A.B.C. Murders"
18      },
19      {
20        "name": "Murder on the Orient Express"
21      },
22      {
23        "name": "And Then There Were None"
24      },
25      {
26        "name": "Endless Night"
27      }
28    ]
29  }
30}

Resolvers

We can also create custom fields that aren’t included in the database. What we do with those fields is up to us. We define which data they return by using resolve functions. Resolvers take two properties: the parent property, and the other one is the arguments property, which we will talk more about later. Here we will create an author field in the book type, which finds the author of the queried book using the resolve function, whose parent property we named - book:

1const BookType = new GraphQLObjectType({
2  name: "Book",
3  description: "A single book",
4  fields: () => ({
5    id: { type: GraphQLInt },
6    authorId: { type: GraphQLInt },
7    name: { type: GraphQLString },
8    year: { type: GraphQLInt },
9    author: {
10      type: AuthorType,
11      resolve: book => authors.find(author => author.id === book.authorId)
12    }
13  })
14})

Now we can access all the book along with their respective authors:

1{
2  books {
3    name
4    author {
5      name
6    }
7  }
8}
1{
2  "data": {
3    "books": [
4      {
5        "name": "The Shining",
6        "author": {
7          "name": "Stephen King"
8        }
9      },
10      {
11        "name": "It",
12        "author": {
13          "name": "Stephen King"
14        }
15      },
16      
17      ...
18      
19      {
20        "name": "And Then There Were None",
21        "author": {
22          "name": "Agatha Christie"
23        }
24      },
25      {
26        "name": "Endless Night",
27        "author": {
28          "name": "Agatha Christie"
29        }
30      }
31    ]
32  }
33}

Arguments

Another great thing we could do is to pass arguments to our queries. This allows us to be even more specific about the data that we want to fetch. For example, we could request books but only from a certain author. The first thing we need to do is create a resolver that lets us access all the books of a certain author:

1const AuthorType = new GraphQLObjectType({
2  name: "Author",
3  description: "A single author",
4  fields: () => ({
5    id: { type: GraphQLInt },
6    name: { type: GraphQLString },
7    books: { 
8      type: new GraphQLList(BookType) ,
9      resolve: author => books.filter(book => book.authorId === author.id)
10    }
11  })
12})

Next, we create a new field in the RootQueryType, which we will use to access only certain author data. The way we do this is bypassing an argument through the query. The argument gets resolved by the, you guessed it, resolver function. We pass the arguments in as the second property because the first one is the parent property. We also need to define which properties the arguments object has, along with its types:

1const RootQueryType = new GraphQLObjectType({
2  name: 'Query',
3  fields: () => ({
4    author: {
5      type: AuthorType,
6      args: {
7        name: { type: GraphQLString }
8      },
9      resolve: (parent, args) => authors.find(author => author.name === args.name)
10    },
11    authors: {
12      type: new GraphQLList(AuthorType),
13      description: 'A list of all authors',
14      resolve: () => authors
15    },
16    books: {
17      type: new GraphQLList(BookType),
18      description: 'A list of all books',
19      resolve: () => books
20    }
21  })
22})

Now we can easily fetch books only from a certain author:

1{
2  author(name: "Stephen King") {
3    name
4    books {
5      name
6    }
7  }
8}
1{
2  "data": {
3    "author": {
4      "name": "Stephen King",
5      "books": [
6        {
7          "name": "The Shining"
8        },
9        {
10          "name": "It"
11        },
12        {
13          "name": "Pet Sematary"
14        },
15        {
16          "name": "Misery"
17        }
18      ]
19    }
20  }
21}

Mutations

Earlier, we talked about different operation types, and we mentioned that when we only want to fetch data from the server, we use the query operation type. Still, since that is the default operation type, we don’t have to declare it specifically. There are situations in which we would need to edit the data on the server, and that is done by using the mutation operation type. We need to define it as a new field in our schema object and create its own type, which we will call RootMutationType:

1const RootMutationType = new GraphQLObjectType({
2  name: 'Mutation',
3  fields: () => ({
4    addBook: {
5      type: BookType,
6      description: 'Create new book',
7      args: {
8        name: { type: GraphQLString },
9        year: { type: GraphQLInt },
10        authorId: { type: GraphQLInt }
11      },
12      resolve: (parent, args) => {
13        const newBook = {
14          name: args.name,
15          year: args.year,
16          authorId: args.authorId,
17          id: books.length + 1,
18        }
19        books.push(newBook)
20        return newBook
21      }
22    }
23  })
24})
25
26const schema = new GraphQLSchema({
27  query: RootQueryType,
28  mutation: RootMutationType
29})

In the fields object, we define all operations that can be used to mutate the server data. We created the addBook operation, which takes three arguments: name, year, and authorId. We create new books using the data we get from these arguments, and then we add the new book to our server data. The response that we get from this kind of request is the object that we had just created:

1mutation {
2  addBook(authorId: 1, name: "The Stand", year: 1978) {
3    name
4    year
5    author {
6      name
7    }
8  }
9}
1{
2  "data": {
3    "addBook": {
4      "name": "The Stand",
5      "year": 1978,
6      "author": {
7        "name": "Stephen King"
8      }
9    }
10  }
11}

Now, if we check how the data on the server looks like, it should contain the newly created book:

1{
2  books {
3    name
4    author {
5      name
6    }
7  }
8}
1{
2  "data": {
3    "books": [
4      {
5        "name": "The Shining",
6        "author": {
7          "name": "Stephen King"
8        }
9      },
10      {
11        "name": "It",
12        "author": {
13          "name": "Stephen King"
14        }
15      },
16      
17      ...
18      
19      {
20        "name": "And Then There Were None",
21        "author": {
22          "name": "Agatha Christie"
23        }
24      },
25      {
26        "name": "Endless Night",
27        "author": {
28          "name": "Agatha Christie"
29        }
30      },
31      {
32        "name": "The Stand",
33        "author": {
34          "name": "Stephen King"
35        }
36      }
37    ]
38  }
39}

Conclusion

As you can see, using GraphQL is a bit confusing at first, but the more you use it, the more you realize how much power it has. Compared to REST APIs, it is much faster to be picky about the data you request. GraphQL also creates a single endpoint /graphql, compared to RESTful APIs, which forces you to create many endpoints that provide a lot of data, and sometimes you don’t need all of that information. This is called overfetching.

This was, of course, just a basic introduction to GraphQL. There is a lot more that this technology can provide you with, and I highly suggest digging a little deeper into it.

You can find all the code written in this tutorial here:

https://github.com/nikola1912/getting-started-with-graphql

Nikola Jovanovic

2021-03-16

Nikola is software engineer with a problem solving mindset, in love with JavaScript and the whole ecosystem. Passionate about frontend and backend work.

See more blogs:

Leave your thought here

Your email address will not be published. Required fields are marked *