This is the second tutorial in the series about GraphQL. In this tutorial, we are going to learn more about how GraphQL works. First, we’re going to start with Types, and then dive deep into Queries.
Getting Started
In this series, we’re not going to learn how to set up a GraphQL server. We’re going to learn more about how queries work in GraphQL. If you don’t know how to set up a GraphQL server, that's not going to be a problem at all, you can simply clone this repository which I made just for this series.
When you clone the repo, just run npm install
or yarn
install
. After that, start the server by running npm start
, and go to localhost:4000/playground
.
If you see this screen, this means you’re running the playground successfully. The GraphQL Playground is an IDE for working with GraphQL APIs to write queries, mutations & subscriptions with features like auto-completion, error handling and team collaboration. It makes it possible for us to easily test our APIs, see if there are any errors, and so on.
Now that we’re running it, we’re ready to learn more about types, queries, and mutations.
Go to our graphql
folder inside src
, and you will see that we have two folders: Author
and Book
. These two folders are going to contain our types and resolvers.
For each folder, we’re going to have two files: resolvers.js
and typeDefs.js
.
Basically, the resolvers.js
is going to contain all of our code such as queries, mutations, and subscriptions.
The typeDefs.js
is used only for the types needed to create our schema. So, every time you hear someone talking about resolvers or typeDefs in GraphQL, remember that.
typeDefs
= a file that contains our types.
resolvers
= a file that contains queries, mutations, and
subscriptions.
Types in GraphQL
To write our first set of queries, we need to know a little bit about types, since we’re going to use it very often in GraphQL.
What are types in GraphQL and why do they matter?
Types in GraphQL are custom objects that define the way your API is going to look. Pretty simple! You can define as many types as you want, for every part of your API.
All types must have fields, so you need to pass the exact fields that you want, and define the type of data that those fields will have.
Let's write our first Type in GraphQL.
Inside the Author
folder, you’re going to see that we already have one type called Author
, and we’re going to add some new fields. This type looks like this:
const typeDefs = `
type Author {
_id: ID!
firstName: String!
lastName: String!
age: Int!
books: [Book!]!
}
`;
export default typeDefs;
Author type pretty self-explanatory. By reading it you can understand that it is an Author
type, and it has a few fields. Each field must have a type. You can define String, ID, Int, Date, or even other Custom Types as well.
The books
field contains an array of Book
, and this Book
is a custom object type that we’re going to define later. Each Author
can have one or more Book
and that’s why you should put it inside brackets.
Those !
at the end of each field mark it as a non-nullable field, which means that the field must return a value as a result of a query. In our Author
type we have marked every field as non-nullable so that each field returns a value.
Now that we defined our Author
type, we’re going to define the other type that we’re going to need which is the Book
type. Go to the Book
folder inside the graphql
folder, and you will see that we have a Book
type. We’re going add some fields to this type, like this:
const typeDefs = `
type Book {
_id: ID!
title: String!
description: String!
language: String!
author: Author!
}
`;
export default typeDefs;
Each Book
is going to have a title, description and a language, and all of them should be a String
. Each Book
is also going to have an Author
, and we defined the type for it a few moments ago. It's pretty similar to the Author
type and doesn’t have much difference besides the fact that each Book
will have only one Author
.
Now that we know how to write types, we’re going to learn more about Queries and write our first Query type.
Queries
The only way we can get data in GraphQL is through Queries. We need to specify what we need to get and from where we need to get the data. It’s not similar to REST because in a REST API we get a fixed structure of data. However, in GraphQL we can get just the data that we want. This solves a lot of problems for us, especially the over-fetching or under-fetching of information.
To make our queries, we need to define the types, after which we’re going to write the code for our queries. So, don’t get confused -- We’re just defining the type of queries and what queries we’re going to have.
So, inside the Author
folder, inside the Query
type, we’re going to add a query called getAllAuthors
, and after that our type is going to look like this:
type Query {
getAuthor(_id: ID!): Author!
getAllAuthors: [Author!]!
}
Our queries live inside a Query
type. So, every time we need to add or remove a query, we will directly go to this type.
Each query can have one or more arguments. In this example we made a query called getAuthor
that needs an _id
to return a specific Author
. The other query is called getAllAuthors
. We’re not going to pass any argument as it returns all authors.
Now, inside the Book
folder, inside the Query
type, we’re going to add another query called getAllBooks
, and after that our type is going to look like this:
type Query {
getBook(_id: ID!): Book!
getAllBooks: [Book!]!
}
Pretty similar to the Author
queries. The query getBook
needs an _id
to return a specific Book
. The other query is called getAllBooks
. We’re not going to pass any argument since we’re just going to return all the books that we have.
Now, we’re going to write the code for our queries. First, go back to the Author
folder, and this time go to resolvers.js
. We’re going to write our
first set of queries. Inside that file paste the following code:
import Author from "../../models/Author";
import Book from "../../models/Book";
const resolvers = {
Query: {
getAuthor: async (parent, { _id }, context, info) => {},
getAllAuthors: async (parent, args, context, info) => {}
},
};
export default resolvers;
We just defined the structure for our resolvers. Now we’re going to write the code. In this series of tutorials I’m going to work with MongoDB (using Mongoose npm module), but with GraphQL you can use any database that you want such as PostgreSQL, MySQL, etc.
So, inside our getAuthor
function, put the following code:
return await Author.findById(_id)
.populate()
.then(author => author)
.catch(err => err);
We’re going to return a specific Author
. So, in the parameters of the function, instead of args, you should pass _id
. Our modified function is going to look like this:
getAuthor: async (parent, { _id }, context, info) => {
return await Author.findById(_id)
.populate()
.then(author => author)
.catch(err => err);
},
Now, we’re going to write another query called getAllAuthors
which is going to return all authors we have. Put the following code inside the function:
return await Author.find()
.populate()
.then(authors => authors)
.catch(err => err);
Now our function should look like this:
getAllAuthors: async (parent, args, context, info) => {
return await Author.find()
.populate()
.then(authors => authors)
.catch(err => err);
}
Inside the resolvers (but not inside the Query object), write the following code:
Author: {
books: async ({ _id }, args, context, info) => {
return await Book.find({ author: _id });
}
}
With this, we make it possible to query the Author
from a specific Book
when making queries. If we don't specify it and try to get the Author
from Book
, it will return an error.
Now, our file should look like this:
import Author from "../../models/Author";
import Book from "../../models/Book";
const resolvers = {
Query: {
getAuthor: async (parent, { _id }, context, info) => {
return await Author.findById(_id)
.populate()
.then(author => author)
.catch(err => err);
},
getAllAuthors: async (parent, args, context, info) => {
return await Author.find()
.populate()
.then(authors => authors)
.catch(err => err);
}
},
Author: {
books: async ({ _id }, args, context, info) => {
return await Book.find({ author: _id });
}
}
};
export default resolvers;
We now defined all queries that our Author type is going to have. We need to define the Book
queries, so go to our Book
folder and inside the
resolvers.js
put the following code:
import Author from "../../models/Author";
import Book from "../../models/Book";
const resolvers = {
Query: {
getBook: async (parent, { _id }, context, info) => {},
getAllBooks: async (parent, args, context, info) => {}
}
};
export default resolvers;
The code is almost the same as the Author
queries, so inside our getBook
, pass an _id
as an argument, and inside the function we’re going to pass the following code:
return await Author.findById(_id)
.populate()
.then(author => author)
.catch(err => err);
And inside thegetAllBooks
query, pass the following code:
return await Book.getAllBooks()
.then(books => books)
.catch(err => err);
Inside the resolvers (but not inside the Query object) write the following code:
Book: {
author: async ({ author }, args, context, info) => {
return await Author.findById(author);
}
}
Now, our code should look like this:
import Author from "../../models/Author";
import Book from "../../models/Book";
const resolvers = {
Query: {
getBook: async (parent, { _id }, context, info) => {
return await Book.getBook({ _id })
.then(book => book)
.catch(err => err);
},
getAllBooks: async (parent, args, context, info) => {
return await Book.getAllBooks()
.then(books => books)
.catch(err => err);
}
},
Book: {
author: async ({ author }, args, context, info) => {
return await Author.findById(author);
}
}
};
export default resolvers;
Pretty simple and concise. Now, we need to test our queries. For that, we need to go to the GraphQL Playground at localhost:4000/playground
, and write our first query to check if it's working.
Inside the playground, we have two sides: the left side, where we write our queries, mutations, and subscriptions, and we also have the right side which shows the result of our requests.
To write a query in GraphQL, we need to pass a string that looks like an object and inside that string, we pass the fields that we like to get.
A simple query in GraphQL looks like this:
query {
getAllBooks {
_id
title
description
language
author {
_id
firstName
lastName
age
}
}
}
First, we specify if it's a query, a mutation or a subscription. Then, we pass the name of the query, since the getAllBooks
query doesn’t have any args that need to pass, we just need to pass inside the fields that we like to get.
Now if you write that query above, and press the button, we’re going to run the query on our server and get the following result:
We get only one Book
that’s because we only have one Book
, for now. Now, we’re going to make a query to getAllAuthors
, and this query is going to look like this:
query {
getAllAuthors {
_id
firstName
lastName
age
books {
_id
title
description
language
}
}
}
If you write this query and run on at the playground, we’re going to get the following result:
Now that we have written our first set of queries, feel free to write one yourself. Try to get a specific Author
by _id
, and a Book
by _id
.
Hint: You can pass a query to specify a parameter, such as this one:
getAuthor(_id: "5c5ed863eb1772069c8d12e6")
This query should return a specific Author
.
Conclusion
In this tutorial, we learned how to write queries in GraphQL. We’re going to learn more about mutations in the next post of this series.
So, stay tuned and see you in the next tutorial!