GraphQL is a query language used to query and retrieve data. It is an alternative to REST and it gives the ability to describe our data as types
since it is a strongly-typed language. We describe a schema and query for the exact data or portion of data we want.
Describe your data (Schema):
type Student {
studentId: ID!
name: String
sex: Male
courses: [Courses]
}
Ask for what you want:
{
student(name: "Ada"){
name
sex
}
}
Retrieve predictable result:
{
student: {
name: "Ada",
sex: "Female"
}
}
Schema
We define a schema
to represent the shape of our data and queries. A schema
can be liken to a blueprint which describes the different types, queries and mutation allowed by our GraphQL server.The GraphQL engine validates any incoming request against our defined schema.
//example schema
type User {
name: String!
userId: ID!
sex: String!
}
In the above example we have a User
type that consist of the name
, userId
and sex
. fields. These fields resolves to a GraphQL built-in scalar type of String
, ID
and String
respectively.
Fields that resolve to a single object and cannot have sub-selection in the query is refer to as a scalar type.
query {
getUsers{
name
userId
sex
}
}
The above query field getUsers
resolves to a concrete data of scalar type of String
, String
and String
for the name
, userId
and sex
fields. These fields do not have a further sub-selection hence the name as scalar. The scalar type represent the leaves of the query.
query {
allBooks{
title #scalar type of string
authors{ #non scalar it has a further sub-selection
name
}
}
}
The above example is a sample query with a field of allBooks
which resolves to a scalar type of string for the title
( no sub-selection) and an authors
fields which has a further selection of name
of the author.
Some examples of GraphQL scalar types are:
- String: A UTFβ8 character sequence.
- Int: A signed 32βbit integer.
- Float: A signed double-precision floating-point value.
- Boolean: true or false.
- ID : This is a unique identifier like a database key
Query and Mutation types
A GraphQL server has a Query
and a Mutation
type. These are special types that define the entry point of every GraphQL query.
type Query {
#all the fields of the query object are listed
allStudents: [User]
student(studentId: String): User
}
type User {
name: String!
age: Int!
sex: String!
studentId: ID!
}
The above describe a GraphQL User
type and queries that can be performed on the server. The field allStudents
in the Query object resolves and returns a list of User
object while the student
returns a single User
object. How does the GraphQL system know how to get the data for these fields?
Resolvers
A resolver is a user defined function that resolves the fields of a type with data. Looking at the Query
object; it consists of two fields which are allStudents
and student
. We will need to define two resolvers function with the same name to pull out the data from our database or store.
//resolvers
Query{
allStudents:() => {
//this function should return a list of users
},
student:() => {
//this should return a single user
}
}
Data can be retrieved from any data source inside a resolver function as GraphQL does not care about the database or source you are using. You only need to point it to the resolver function which it calls to retrieve the data.
Apollo Server
Lets see an implementation of GraphQL using Apollo Server.
We can build a production ready GraphQL server using Apollo Server.
Create a new NodeJS project
- make a new directory by typing:
mkdir graphql_tutorial
- navigate into the new directory by typing:
cd graphql_tutorial
- Navigate to your project directory and type the command
npm init --yes
The above assumes that NodeJS is installed on your machine.
- create a new file named
index.js
touch index.js
- create another file named
data.js
This will serve as the data store to query against.
//data.js file content
const StudentData = [
{
studentId: "1",
name: "John Parson",
sex: "Male",
booksCollection: ["1", "2"]
},
{
studentId: "2",
name: "Pokemon Geshia",
sex: "Female",
booksCollection: ["1"]
},
{
studentId: "3",
name: "Samson Loken",
sex: "Male",
booksCollection: []
},
];
const BooksData = [
{
bookId: "1",
title: "The Law Of The Land",
authors: [
{ authorId: "1", authorName: "John Mason" },
{ authorId: "2", authorName: "Favour Damen" },
],
},
{
bookId: "2",
title: "The Next day is Close",
authors: [{ authorId: "1", authorName: "John Mason" }],
},
];
const AuthorData = [
{
authorId: "1",
name: "John Mason",
sex: "Male",
address: "East West Lane",
books: ["1", "2"]
},
{
authorId: "2",
name: "Favour Damen",
sex: "Female",
address: "Best Avenue Drive Zone",
books: ["1"]
},
];
module.exports = { StudentData, AuthorData, BooksData}
Copy the above code block and dump inside the data.js
file.
- install dependencies
npm install apollo-server-express apollo-server-core express graphql
Run the above command on the terminal to install the packages needed to create the GraphQL server.
- install nodemon to auto run our code
npm install --save-dev nodemon
- open the package.json file and under the scripts section add a start script that will run the server.
{
"name": "graphql_tutorial",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"apollo-server-core": "^3.9.0",
"apollo-server-express": "^3.9.0",
"express": "^4.18.1",
"graphql": "^16.5.0"
},
"devDependencies": {
"nodemon": "^2.0.18"
}
}
- create two files inside the project namely
schema.js
andresolver.js
touch schema.js
touch resolver.js
- open the
index.js
file and put the content below inside.
const { ApolloServer } = require('apollo-server-express');
const { ApolloServerPluginDrainHttpServer } = require('apollo-server-core');
const express = require('express');
const http = require('http');
const resolver = require("./resolvers");
const schema = require("./schema");
async function startApolloServer(typeDefs, resolvers) {
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer({
typeDefs,
resolvers,
csrfPrevention: true,
cache: 'bounded',
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
await server.start();
server.applyMiddleware({ app });
await new Promise(resolve => httpServer.listen({ port: 5000 }, resolve));
console.log(`π Server ready at http://localhost:5000${server.graphqlPath}`);
}
startApolloServer(schema, resolver);
At the top we required Apollo Server and other boiler plate code needed to run the server. Next, we defined an async
function called startApolloServer
which receives as an argument the typeDefs
and resolvers
(schema and resolvers). We are going to define the schema
and resolver
for our Graph and pass it as an argument to the startApolloServer
function. The server is available on http://localhost:5000/graphql
Creating the Schema
We are going to write our schema inside of the schema.js
we created earlier. Our schema will consist of a User
type and some queries and mutation.
const { gql } = require("apollo-server-express");
module.exports = gql`
type Query {
allUsers: [User]
user(studentId: String):User
}
type User {
studentId: ID!
name: String!
sex: String!
}
`
The above schema describes a User
type and two queries field of allUsers
and user
. At the top of the file we required gql
from the apollo-server-express
. The schema is written inside the template string of gql
. The content of the file is exported for use using module.exports
.
We will need to create a resolver function for the fields of allUser
and user
.
Open the file resolver.js
which was created earlier and let's update it with our resolver function.
const { StudentData, BooksData, AuthorData } = require("./data");
module.exports = {
Query: {
allUsers: (parent, args, context, info) => {
return StudentData;
},
user: (parent, args, context, info)) => {
const user = StudentData.find(student =>
student.studentId == args.studentId);
return user;
}
},
A resolver accepts four positional arguments which are :
- parent:
- args:
- context:
- info You can read more about them in the documentation Here
The above resolves retrieves data from our static data file data.js
. The allUsers
resolvers do not take any arguments and return all the list of students. If we are pulling data from a database, this is where we would have performed the queries to retrieve the data. The return value must conform to the schema described.
Lets start and execute the query we just wrote:
Run the server:
At the terminal run:
npm run start
This will start up the server and you will see a message on the terminal : π Server ready at http://localhost:5000/graphql
Open your browser and navigate to http://localhost:5000/graphql
. You will be presented with a playground where you can test your queries and mutation.
Click on the explorer tab to open a new window for you to write out your query.
query{
allUsers {
sex
studentId
name
}
}
Paste the above query inside the tab and click on the Run button. The above query is executed against your server and the result is displayed.
{
"data": {
"allUsers": [
{
"sex": "Male",
"studentId": "1",
"name": "John Parson"
},
<--- rest of the result -->
]
}
}
Apollo server will return a data object with the result of the query. The result of the allUsers
query is an array with the list of the user object (sex,studentId and name). If you look at the file data.js
you will noticed that the StudentData
also consist of a booksCollection
field which is an array of strings. You had notice that the returned data does not contain this field. This is because the booksCollection
field is not defined inside our schema.
Let's write a query to get a particular user. The user
query requires we pass in a studentId
which will be used to fetch the user data.
query($studentId: String){
user(studentId: $studentId) {
name
studentId
}
}
At the top we defined a studentId
variable that is of type string and we passed the value of the variable to the user
field.
The variables are entered below the query tab in the circled location of the image above. Run the query and put in a studentId that existed in the data file. The query runs and we get our result.
Let's introduce some new types into our schema. We want to add a Book
and an Author
type to the schema. We will want to be able to query for the books owned by a student. In the StudentData
array in the data.js
file, we have a booksCollection
which is a list of books by ID
owned by a user.
Open the schema.js
file and add the following code to it.
type User {
#previous fields above
booksCollection: [Book] #added the field booksCollection
}
type Book {
bookId: ID!
title: String!
authors: [Author]
}
type Author {
authorId: ID!
name: String
sex: String
address: String
}
We have added a Book
type and an Author
type to the schema as well as extend the User
type by adding a booksCollection
field. Looking at the new types added, we can notice that the User
type has a field called booksCollection
which resolves into a list of Book
.
The Book
type has a field called authors
that resolves into a list of Author
type. We have to tell our GraphQL how to retrieve the values for these listed fields. That is where our resolvers come in, we will need to add functions that the GraphQL server calls when it encounters these fields so as to resolve them.
Open the file resolver.js
and let's add the resolvers for these field.
{
//resolver.js
//rest of the resolvers file
Book: {
authors: (book) => {
const authors = AuthorData.filter((author) => {
return author.books.includes(book.bookId);
});
return authors;
},
},
User: {
booksCollection: (parent) => {
const student = StudentData.find(s => s.studentId == parent.studentId);
const booksArray = student?.booksCollection;
const collection = [];
booksArray?.map((bookId) => {
const book = BooksData.find(e => e.bookId ==
bookId);
collection.push(book);
});
return collection;
}
}
}
In the Book
type we have a field called authors
that we will need to write a function for so as to retrieve the authors of a book. Remember that a resolver function takes 4 positional arguments of which the first is refer to as the parent of the field, we are resolving. In the case of the authors
resolver the parent is a book
field that we want to resolve its list of authors
.
The authors
resolver returns a list of authors from the AuthorData
array where the book.bookId
is included in the author.books property of the AuthorData
array. It simply means, return all the list of authors that wrote the particular book.
Same above also applies to the booksCollection
field of the User
type which is resolved by finding the user
by the studentId
of the parent. The parent is the User
collection. (we could as well name parent as user ). Once the user is found, the books owned by the user is retrieved by mapping
over the student.booksCollection
array. For each item, in the array, we retrieve the corresponding book and save it into another array called collection
. Once we are done with the loop, we return the collection
array.
Let's run another query to retrieve a particular user and their collection of books.
query($studentId: String){
user(studentId: $studentId) {
name
studentId
sex
booksCollection {
title
authors {
address
sex
authorId
name
}
}
}
}
Put a valid value for the studentId
. The GraphQL server runs the query and makes use of the defined resolver above and returns a user with the complete list of all the books owned.
We have seen how to retrieve data using the query object of GraphQL. Now, let us see how to mutate or change data in our data store.
Mutation
To change data in our server, we will add the Mutation
type. This type has fields which when resolved performs some changes to the data store.
Open the schema.js
file and lets add the Mutation
type.
#rest of the code above
type Mutation {
saveUser(studentId: ID!, name: String!, sex: String!,
collection: [String]): UserDataSaved
}
interface MutationResponse {
code: String!
success: Boolean!
message: String!
}
type UserDataSaved implements MutationResponse {
code: String!
success: Boolean!
message: String!
user: User
}
#rest of the code below
Add the above code after the Query
type. Looking closely at the added code, you will notice that we added an interface
defined which we call MutationResponse
. An interface
in GraphQL is similar to interfaces in other programming language. It is a contract that an implementing type must execute, meaning any type that implement this particular interface must define a code
, success
and message
field.
We defined a UserDataSaved
type that implements the MutationResponse
. To fulfil the interface contract we added the fields of the interface to the type and also added the user
field which is the new user added.
When making a mutation, it is recommended to return the new object that was added or changed in the response.
The Mutation
type has a field called saveUser
which accepts four variables and it resolves with the UserDataSaved
type. Let us write the resolver function for the saveUser
Open the resolver.js
file and create a Mutation
object:
Mutation: {
saveUser: (_, { studentId, name, sex, collection }) => {
const newuser = {
studentId,
name,
sex,
collection
};
StudentData.push(newuser);
return {
code: "200",
success: true,
message: "New user record saved",
user: newuser
}
},
},
The saveUser
resolver accepts as argument, the studentID
,name
, sex
and collection
as arguments. Remember that the second positional arguments of the resolver function is the args
object that is passed to the resolver. We destructured the args
object to retrieve the studentId
, name
, sex
and collection
. We created a newUser
variable using these values and we push it into the StudentData
array. we return an object which matches our schema as response to the mutation.
Let add a new user via the Apollo playground.
mutation($studentId: ID!, $name: String!, $sex: String!, $collection: [String]){
saveUser(studentId: $studentId, name: $name, sex: $sex, collection: $collection) {
code
message
success
user {
name
studentId
booksCollection {
authors {
address
}
}
}
}
}
After running the query, we get a response back showing that the user has been added to our student list. Note that the added user is to persisted to file. We can query for our newly added user.
We have just covered the basics of GraphQl, in the next post we will explore further and learn more about GraphQL. The code for this tutorial can be found Here
Thanks for reading!
Top comments (1)
Great GraphQL article.