A comprehensive introduction to GraphQL: for efficient data queries and mutations
Emmanuel Fordjour Kumah
Posted on March 27, 2024
This article will teach you the concepts in GraphQL to enable you to perform queries and mutations on a data set. GraphQL is a query language and specifications for APIs that enable clients to request specific data, promoting efficiency and flexibility in data retrieval.
At the end of this article, you will know:
What GraphQL is
The problem GraphQL solves
The difference in using HTTP methods in REST API vrs query and mutation in GraphQL API to fetch and manipulate data.
How to define the shape of the data to query using Schema
How to query and mutate data on a GraphQL server using ApolloSandbox
In this tutorial, you will learn how to fetch and mutate using the local data of students and course array. The complete code snippet is on Github
Prerequisite
Familiarity with JavaScript
Familiarity with NodeJS
Introduction to GraphQL
GraphQL is a query language for your API, and a server-side runtime for executing queries using a type system you define for your data.
This means it provides a way to request and modify data from multiple data sources ( for instance, a REST API, SQLite database, and services) in a single query
GraphQL adopts the concept of a graph an approach of interconnecting different data to form a relationship. In modern applications, numerous data can be connected to form the app data graph. For instance, if you are building a blogging app that has resources (data) such as:
Posts
Authors
Comments
Followers
These entities can be connected to form a relationship. Whenever a developer wants to access a resource, he writes a query that specifies exactly what data he needs.
GraphQL is akin to visiting a restaurant, and telling the chef exactly what you want for your menu rather than accepting the provided menu.
For your menu, you can specify:
The type of protein (chicken, fish, tofu) to be served
The cooking method (grilled, baked, etc) to be used.
The sides ( salad, veggies, etc) to be added
And any specific sauces or toppings.
Similarly, with GraphQL, you can request specific fields from different parts of a data. The server then fulfills your request, giving you the exact data you need, without any unnecessary information.
There are two main components of GraphQL:
GraphQL Server
GraphQL Client
The GraphQL server is responsible for implementing the GraphQL API on the server side. Examples of GraphQL servers are:
Express
Apollo server
The GraphQL client allows apps to interact with the GraphQL server. It enables fetching and updating data in a declarative manner.
Examples of GraphQL clients are:
Relay
Apollo Client
What problem does GraphQL solve: data fetching with REST API vrs GraphQL
Generally, front-end developers display data on the UI of an app by sending requests to multiple endpoints.
For instance in building a blogging app. You will have a screen that displays the titles of the posts of a specific user. The same screen also displays the names of the last 3 followers of that user.
With a REST API, these resources may be on different endpoints. To fetch these resources, you may need to send different requests.
For instance, here are the steps to fetch and display the data in the blogging app using a REST API:
Fetch request to
/users/<id>
endpoint to fetch the initial data for a specific user.Another request to the
/users/<id>/posts
endpoint is to return all the posts and then filter out the title.Finally, a request to
/users/<id>/followers
endpoint to return a list of the followers for the specified user.
In this scenario, you are making three requests to different endpoints to fetch the required data to build the UI.
This introduces two issues:
Over-fetching
Under-fetching
Overfetching is when a client downloads additional information than is required in the UI. For instance in the /users/<id>/posts/
endpoint, the data required is the title
for each post. However, additional information may be returned such as:
id
post body,
likes, etc.
These will not be displayed in the UI, and can lead to:
Unnecessary transmission of data over the network.
Increased response times, and possibly impacting the performance of the client app.
Underfetching is when a client needs to make multiple requests to the server to gather all the necessary data for a particular view or operation. This happens when a specific endpoint doesn’t provide enough of the required information hence the client will have to make additional requests to fetch everything it needs.
Underfetching can lead to unnecessary network calls as numerous exchanges between the client and server increase the request time and network traffic.
GraphQL solves these issues of underfetching and overfetching by enabling clients to specify their exact data requirements in a *single query*. A "single" query means only one request is made to a GraphQL server to fetch specific data. That query will contain all the data required by the client.
Understanding the client and server roles
The GraphQL API is built on top of a GraphQL server. The server is the central point for receiving GraphQL queries. Once the query is received, it will be matched against a defined schema (you will learn more about schema as you proceed). The server retrieves the requested data by interacting with databases, microservices, or other data sources.
The GraphQL server consists of:
Schema: This represents the structure or shape of your data set.
Resolvers: They are functions that specify how to process specific GraphQL operations and contain the logic for fetching the requested data.
Data Sources: any available data source. Eg. MySQL, MongoDB, etc
The GraphQL Client enables apps to interact with a GraphQL API. You will describe the data your app needs in the GraphQL client, send queries and mutations to the server, and receive the response.
Examples of GraphQL clients are:
Apollo Client
Fetch QL
Relay
So far, you know what GraphQL is and the problem it solves. In the next section, you will learn how to build a GraphQL Server using the Apollo server.
Setting up a GraphQL Server
In this section, you will:
Build and run an Apollo Server
Define the GraphQL Schema that represents the structure of your data sets.
Let's get started
Step 1: Creating a new project
Create a directory and navigate into the directory
-
Inside the directory, initialize a Node.js project and set it up to use
ES
modulesnpm init --yes && npm pkg set type="module"
-
Install
graphql
andapollo-server
dependencies using the command below:npm install @apollo/server graphql
Step 2: Create an instance of the Apollo server
Create an
index.js
file inside the root directory. This will contain all the server requirements.-
Add the code below to the
index.js
fileimport { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; const server = new ApolloServer({ //typeDefs, //resolvers, }); const { url } = await startStandaloneServer({ server, listen: { port: 4000 }, }); console.log(`Server ready on port ${url}`);
In the code above:
The
new ApolloServer()
constructor creates a new instance of the server. This accepts anobject
with two properties: the schema definition and the set of resolvers.Next, you will pass the server instance to the
startStandaloneServer
function. This will create an Express server, install the ApolloServer instance as middleware, and prepare your app to handle incoming requests.
Step 3: Define your GraphQL Schema and types
The GraphQL schema specifies the available data, its format (data types), and the operations(queries, mutation) that can be performed on it. When the query is initiated the data returned should match the structure defined in the schema.
Here is what you should know about a schema:
The schema is a collection of types (and the relationship between these types).
-
Every type you define can be categorized into:
- Scalar: This is similar to the primitive types in JavaScript (
String
,Float
,Int
,Boolean
,ID
, etc) - Object: This represents the core items that can be fetched and what fields it has.
- Scalar: This is similar to the primitive types in JavaScript (
-
The schema also has two special types:
-
Query
andMutation
: These detail what data you can request or mutate. You will learn more aboutqueries
andmutations
types later.
-
Below is the syntax of an object type definition:
type EntityName {
#field name and the corresponding type
fieldName: scalarType
}
In this tutorial, clients can query from an array of students and course data. Hence, you will define the structure for the Course
and Student
as object type in the schema.
Here's an example of a Course
object in a schema definition.
# Example of schema defintion
# This represents a Course object with fields that object has
type Course {
id:ID
name:String
cost:String
rating: Float
}
Let's examine the type above:
Course
is the name of the object type.The details between the
{
and}
are fields of theCourse
type. That meansid
,name
,cost
, andrating
are the only visible fields in any GraphQL query that works on theCourse
object.The
ID
,String
andFloat
are the scalar types. It means the returned data for the field should be of typeString
,Float
orID
Now, let's create a schema.js
file in the root directory, and define a variable called typeDefs
to store all our types
in the schema. Later, you will pass this typeDefs
to the server.
#schema.js
export const typeDefs = `#graphql
# Your schema will go here
`;
export const typeDefs = `#graphql
# define the Course object
type Course {
id:ID
name:String
cost:String
rating: Float
}
# define the Student type
type Student {
id: ID
name: String
level: String
courses:[String]
}
`;
In the above, we have two objects in our schema: Course
and Student
each with its structure.
Understanding theQuery
Type
Let's tell GraphQL what to retrieve when we query. The Query
type indicates what you can fetch from the data source.
Here is what you need to know about the Query
type:
It can be compared to the 'read' operations in a CRUD (Create, Read, Update, Delete) system.
In the
Query
type arefields
that acts as entry points into the rest of our schema.
The advantage of GraphQL is that you can fetch data from various resources using a single query ( Query type). However, with REST APIs different endpoints are required to fetch various resources (e.g api/students
, api/courses
).
Below is the syntax for a Query
:
# This is the Root Query
type Query {
# within indicate the specify fields to query and what the expected results should be
field: returnedType
field: returnedType
}
On the front-end, we want to fetch an array of courses, students, and the details of a specific student
Here is how we will define them in our root Query
# Use the Root Query to define what you can fetch from the data source
type Query {
#get courses array
courses: [Course]
#get students array
students:[Student]
#Fetch a specific student by providing a student's ID as argument
student(id:ID!): Student
}
In the code above, clients will be able to execute a single query by specifying the courses
and students
fields.
courses
: This returns an array of courses of typeCourse
students
: This returns an array of students of typeStudent
student
: This field accepts anid
parameter and returns specific student details
Step 4: Define your data set
In the previous steps, we define the structure of our data and what we can query. Now, we will define the data itself.
Apollo Server can fetch data from any source you connect to ( for instance, SQLLite database, REST API, etc).
In this tutorial, you will use a local data source.
Create a
db.js
file in your root directoryAdd the code below
let courses = [
{
id: "1",
name: "Web Development",
cost: "300",
rating: 4.5,
},
{
id: "2",
name: "Digital Marketting",
cost: "230",
rating: 3.0,
},
{
id: "3",
name: "Data Analytics",
cost: "345",
rating: 3.9,
},
{
id: "4",
name: "Cyber Security",
cost: "341",
rating: 3.1,
},
{
id: "5",
name: "Mobile Apps",
cost: "465",
rating: 2.1,
},
{
id: "6",
name: "Artificial Intelligence",
cost: "604",
rating: 5.0,
},
{
id: "7",
name: "Maching Learning",
cost: "345",
rating: 2.5,
},
{
id: "8",
name: "Dev Ops",
cost: "567",
rating: 2.6,
},
{
id: "9",
name: "Backend Development",
cost: "345",
rating: 3.1,
},
];
let students = [
{
id: "1",
name: "Emmanuel Smith",
level: "100",
courses: ["Web Development", "Dev Ops"],
},
{
id: "2",
name: "Robert Taylor",
level: "200",
courses: ["Backend Development", "Machine Learning"],
},
{
id: "3",
name: "Emly Lastone",
level: "100",
courses: ["Frontend Development"],
},
{
id: "4",
name: "Clement Sams",
level: "300",
courses: ["Mobile Apps"],
},
{
id: "5",
name: "Lius Gracias",
level: "100",
courses: ["Machine Learning", "Backend Development"],
},
{
id: "6",
name: "Jeniffer Baido",
level: "200",
courses: ["Data Science"],
},
{
id: "7",
name: "Natash Gamad",
level: "300",
courses: ["Cyber Security"],
},
{
id: "8",
name: "Paul Graham",
level: "100",
courses: ["Web Development"],
},
{
id: "9",
name: "Musti Madasd",
level: "300",
courses: ["Artiificial Inteligence"],
},
{
id: "10",
name: "Victor Bruce",
level: "200",
courses: ["Mobile Apps", "Backend Development"],
},
{
id: "11",
name: "Lilian Taylor",
level: "200",
courses: ["Web Development"],
},
{
id: "12",
name: "Smith Chef",
level: "100",
courses: ["Backend Development"],
},
];
export default { courses, students };
Step 5: Add the typeDefs to the server
The Apollo Server needs to know about the types defined in the schema.js
.
In the index.js
file:
import the
typeDefs
specified in theschema.js
Pass the
typeDefs
as an argument to thenew ApolloServer({})
#index.js
...
import { typeDefs } from "./schema";
const server = new ApolloServer({
typeDefs, #type defs passed as an argument
#resolvers will be passed here later
});
....
Next, we will define the logic for querying and mutating the data. To accomplish this, you will use resolvers
Step 6: Set up a resolver
Resolvers are functions that generate a response for every GraphQL query. A resolver's mission is to populate the data for a field in your schema. It connects schema
with the data sources, fetches the requested data, and populates the fields in the schema with that data. Resolvers have the same name as the field that it populates data for.
In the schema.js
you have the Query type below:
#schema.js
type Query {
# specify fields to query and what the expected results should be
courses: [Course]
students:[Student]
}
You will define resolvers for the courses
and students
fields of the root Query
type so that they always return an array
of Course
and Student
when queried.
Add the code below to the index.js
file
//index.js
const resolvers = {
Query: {
//resolver function for students field
students() {
//connect to the data source and return students data
return db.students;
},
// resolver function for courses field
courses() {
// connect to the data source and return courses data
return db.courses;
},
},
};
Here are the steps to define a resolver:
Define all the resolvers in a JavaScript object named
resolvers
. This object is called the resolver mapThe
resolver
will have a key ofQuery
with an object valueWithin the object, you will define a function that connects to the data source performs the needed operation, and returns the specified data. The function name should be the same as the field to query
In the code above the students
and courses
resolver functions connect to the data in the db.js
file, and return the students
and courses
data respectively.
Next, you will pass the resolvers
object to the server:
...
// Pass schema definition and resolvers to the ApolloServer constructor
const server = new ApolloServer({
typeDefs,
resolvers,
});
...
Let's recap what we have done:
Created an Apollo server
Defined the schema
Defined the resolver
Connected the resolver to the data
Passed the resolver and schema to the Apollo server instance
In the next sections, you will start the server, query, and mutate data from the source.
Step 8: Start the server
Let's start the Apollo server by running the command below:
node index.js
// if you have nodemon install use the command
nodemon index.js
You should now see the following output at the bottom of your terminal:
🚀 Server ready at: http://localhost:4000/
Fetching resources from the server
Our server is up and running. Now, we need to fetch data from the source. To do that, you will execute GraphQL queries on the server. Queries are operations that fetch resources from the server.
Because we do not have a frontend app, we will use Apollo Sandbox to execute the query. It provides a quick way to test GraphQL endpoints.
Visit http://localhost:4000
in your browser to open the sandbox. The Sandbox includes the Apollo Studio Explorer, which enables you to build and run operations on the Apollo server.
The Sandbox UI displays:
A middle panel for writing and executing queries
A right panel for viewing responses to the query results
Tabs for schema exploration, search, and settings
A URL bar for connecting to other GraphQL servers (in the upper left)
Let's learn how to fetch data from the GraphQL server.
Below is the syntax for a query:
query Query_name{
someField
}
Now, Let's execute a query for the students
and courses
fields
Enter the
query
keywordType the name for your query ( e.g StudentQuery, CoursesQuery, etc)
Within the curly brackets (
{}
), enter the name of the field to query as defined by the rootQuery
in your schemaOpen another curly bracket (
{}
) and specify the exact fields to query for the object
Because you are using the ApolloSandbox, you can enter these steps in the "Operations" panel and click the "blue" button in the upper right.
Now, type the code below into the Operations panel and click the blue button.
# executing a query
query CourseStudentsQuery{
courses {
# only get these fields from the query
name
cost
}
students {
# only get these fields from the query
name
courses
}
}
This action will:
connect to the server
call the resolver function for that field.
The resolver function connects to the data sources, performs the needed logic, and returns a response
The response is a JSON object containing only the specified fields.
The response will appear in the "Response" panel
Here is a screenshot of the operations and the response
One advantage of GraphQL is enabling clients to choose to query only for the fields they need from each object
In the query above, we requested only these fields for each query
courses:
name
andcost
fieldsstudents:
name
andcourses
fields
Hence, only these fields will show in the response. Now, we have fetched the specified data for two different resources with a single query eliminating over-fetching.
Understanding GraphQL arguments: querying for a specific field
In our schema, we have defined the entry points for courses and students, but we still need a way to query for a specific student by its ID. To do that, we'll need to add another entry point to our schema.
An argument is a value you provide for a particular field in your query to help:
retrieve specific objects
filter through a set of objects
or even transform the field's returned value.
To define an argument:
Add parentheses after the field name. Eg. fieldName()
Inside the parentheses, write the name of the argument followed by a colon, then the type of that argument. Eg. (
id:ID
). Separate multiple arguments with commas.
Below is the syntax
type Query {
fieldName(argName: argType)
}
In this tutorial, we want to query for a specific student by ID
Inside the Query
type in schema.js
:
Add the
student
fieldWithin the parenthesis specify the
id
argument and its type (id
: ID)Specify the returned object type
Student
type Query {
#initial queries remains
...
#Fetch a specific student by providing a student's ID as argument
student(id:ID!): Student
}
Next, in the index.js
define a resolver for the student
field that uses the provided ID to fetch details of the student.
//index.js
const resolvers = {
Query: {
...
//resolver for the student field to fetch a specific student
student(parent, args, contextValue, info) {
return db.students.find((student) => student.id === args.id);
},
},
};
A resolver can optionally accept four positional arguments: (parent, args, contextValue, info)
The
args
argument is an object that contains all GraphQL arguments provided for the field in the schema.Inside the body of the resolver function, we connect to the database and find a student with
id
that equals theargs.id
Because we have defined the resolver function for the student, we can query for a specific student with the ID.
Here is how the operation will look like in ApolloSandbox
In the "Variables" section, we have a variable
studentId
with a value of 5In the "Operation" section is
Student
query. It accepts the variable$studentId
with a type ofID!
The
$
indicates a variable and the name after indicates the variable nameNext, we specify the field to query and pass in the variable
We then specify that we only want to return the
name
field for that query
You know how to fetch data from the server using the ApolloSandbox. Next, let's learn how to mutate data.
Mutating data on a GraphQL server
A mutation is an operation that allows you to insert new data or modify the existing data on the server. This is similar to POST
, PUT
, PATCH
and DELETE
requests in REST
To modify data, you use the Mutatation
type.
Here is how to mutate data in our schema:
Start with the
type
keyword followed by the nameMutation
Inside the curly braces, specify the entry point. That is the field to mutate.
We recommend using a verb to describe the specific action followed by the data the mutation will act on. Eg.
addStudent
,deleteStudent
,createStudent
Pass the ID as an argument to the field to mutate
Specify the return type after the user makes the mutation.
Let's delete a student from the database. Here is how to do that
#schema.js
type Mutation{
#entry point to delete a student
deleteStudent(id: ID!): [Student]
}
In the code above:
We use
deleteStudent(id: ID!)
field to indicate we want to delete a student's data by passing theid
as an argument.Once the user has deleted a student, we return updated data that specifies an array of
Student
object.
Next, you will define the resolver function for this mutation. Inside the index.js
and in the resolver
object, type the code below:
//index.js
const resolvers = {
//code here remains the same
...
//The Mutation object holds the resolver methods for all mutations
Mutation: {
//function to mutate data of a specified student
deleteStudent(_, args) {
db.students = db.students.filter((student) => student.id !== args.id);
return db.students;
},
},
};
In the code above:
We added a
Mutation
property to theresolvers
object to indicate the mutation of dataWe defined the
deleteStudent(_, args)
method and passed theargs
object as parameterInside the method, we access the database and filter out all students whose id does not match the
args.id
Finally, we returned the results.
Now, let's delete a student using the Sandbox.
Open a new tab and in the "Operation" section, add the following code below:
mutation DeleteMutation($deleteStudentId: ID!){
deleteStudent(id: $deleteStudentId) {
//Student object should have the following fields
id,
name,
level,
}
}
In the above:
We used the
mutation
key to indicate a mutation of dataDeleteMutation
is the general name for the mutation. We pass the$deleteStudentId
variable as a parameter. This will hold the value passed in the 'Variable" section of the ApolloSandboxThe
deleteStudent(id: $deleteStudentId)
is the field defined in the schema. It accepts the ID we passed in the "Variable" sectionWithin the body of the
deleteStudent
we specify the fields theStudent
object should return after deleting
Finally, in the "Variable" section, indicate the ID of the student to delete and click on the "DeleteMutation" button. The specified student will be deleted and the response is displayed in the "Response" section.
Let's learn how to add a new student.
-
In the
Mutation
type, defineaddStudent
field. This field will accept astudent
argument of typeStudentInputs
and return aStudent
type
Add the code below the Mutation
:
#schema
type Mutation{
# add addStudent field
addStudent(student: StudentInput!): Student
}
In the next line after the Mutation
, define the StudentInput.
This will contain all the fields the new student will have
#Mutating data
type Mutation{
deleteStudent(id: ID!): [Student]
addStudent(student: StudentInput!): Student
}
# data to pass to addStudent
input StudentInput{
name: String
level: String
courses:[String]
}
Next, go to the index.js
and in the Mutation
property of the resolvers
object, we will define a addStudent
resolver function that adds a new student to the data.
Add the code below the Mutation
:
//index.js
const resolvers = {
Query: {
...
},
//The Mutation object holds the resolver methods for all mutations
Mutation: {
...
// this will add new student to the students data
addStudent(_, args) {
let student = {
...args.student,
id: Math.floor(Math.random() * 1000).toString(),
};
db.students.push(student);
return student;
},
},
}
In the code above:
Within the body of the
addStudent
method, we have defined astudent
objectThe
args.student
contains all the arguments we will pass to the method and theid
will generate a random IDThe
db.student
access the students' data source, and we push the newstudent
object to itFinally, we return the details of the student we created.
Here is the screenshot on how to add a new student in the sandbox:
The
AddMutation
accepts a$student
variable of typeStudentInput
The
$student
is passed to theaddStudent
functionIn the "Variable" section, we define the input data. This is a student object with
name
,level
andcourses
properties.
Summary
You now have the required knowledge to query and mutate data with GraphQL. In summary, GraphQL is a query language for API that provides a way to request and modify data from multiple data sources in a single query.
For further studies, check this comprehensive tutorial on ApolloGraphql
Posted on March 27, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
March 27, 2024