Chapter 4. Our First GraphQL API
Presumably, if you are reading this, you are a human being. As a human you have a number of interests and passions. You also have family members, friends, acquaintances, classmates, and colleagues. Those people also have their own social relationships, interests, and passions. Some of these relationships and interests overlap, while others do not. All together, we each have a connected graph of the people in our lives.
These types of interconnected data are exactly the challenge that GraphQL initially set out to solve in API development. By writing a GraphQL API we are able to efficiently connect data, which reduces the complexity and number of requests while allowing us to serve a client precisely the data they need.
Does that all sound like a bit of overkill for a notes application? Perhaps it does, but as you’ll see, the tools and techniques provided by the GraphQL JavaScript ecosystem both enable and simplify all types of API development.
In this chapter we’ll build a GraphQL API, using the apollo-server-express
package. To do so, we’ll explore fundamental GraphQL topics, write a GraphQL schema, develop code to resolve our schema functions, and access our API using the GraphQL Playground user interface.
Turning Our Server into an API (Sort Of)
Let’s begin our API development by turning our Express server into a
GraphQL server using the apollo-server-express
package.
Apollo Server is an
open source GraphQL server library that works with a large number of
Node.js server frameworks, including Express, Connect, Hapi, and Koa. It enables us to serve data as a GraphQL API from a Node.js application and also provides helpful tooling such as the GraphQL Playground, a visual helper for working with our API in development.
To write our API we’ll be modifying the web application code
we wrote in the previous chapter. Let’s start by including the
apollo-server-express
package. Add the following to the top of your
src/index.js file:
const
{
ApolloServer
,
gql
}
=
require
(
'apollo-server-express'
);
Now that we’ve imported apollo-server
, we’ll set up a basic
GraphQL application. GraphQL applications consist of two primary
components: a schema of type definitions and resolvers, which resolve
the queries and mutations performed against the data. If that all sounds like nonsense, that’s OK. We’ll implement a “Hello World” API response and will further explore these GraphQL topics throughout the
development of our API.
To begin, let’s construct a basic schema, which we will store in a
variable called typeDefs
. This schema will describe a single Query
named hello
that will return a string:
// Construct a schema, using GraphQL schema language
const
typeDefs
=
gql
`
type Query {
hello: String
}
`
;
Now that we’ve set up our schema, we can add a resolver that will return a value to the user. This will be a simple function that returns the string “Hello world!”:
// Provide resolver functions for our schema fields
const
resolvers
=
{
Query
:
{
hello
:
()
=>
'Hello world!'
}
};
Lastly, we’ll integrate Apollo Server to serve our GraphQL API. To do
so, we’ll add some Apollo Server–specific settings and middleware and
update our app.listen
code:
// Apollo Server setup
const
server
=
new
ApolloServer
({
typeDefs
,
resolvers
});
// Apply the Apollo GraphQL middleware and set the path to /api
server
.
applyMiddleware
({
app
,
path
:
'/api'
});
app
.
listen
({
port
},
()
=>
console
.
log
(
`GraphQL Server running at http://localhost:
${
port
}${
server
.
graphqlPath
}
`
)
);
Putting it all together, our src/index.js file should now look like this:
const
express
=
require
(
'express'
);
const
{
ApolloServer
,
gql
}
=
require
(
'apollo-server-express'
);
// Run the server on a port specified in our .env file or port 4000
const
port
=
process
.
env
.
PORT
||
4000
;
// Construct a schema, using GraphQL's schema language
const
typeDefs
=
gql
`
type Query {
hello: String
}
`
;
// Provide resolver functions for our schema fields
const
resolvers
=
{
Query
:
{
hello
:
()
=>
'Hello world!'
}
};
const
app
=
express
();
// Apollo Server setup
const
server
=
new
ApolloServer
({
typeDefs
,
resolvers
});
// Apply the Apollo GraphQL middleware and set the path to /api
server
.
applyMiddleware
({
app
,
path
:
'/api'
});
app
.
listen
({
port
},
()
=>
console
.
log
(
`GraphQL Server running at http://localhost:
${
port
}${
server
.
graphqlPath
}
`
)
);
If you’ve left the nodemon
process running, you can head straight to your
browser; otherwise, you must type npm run dev
within the terminal
application to start the server. Then visit http://localhost:4000/api, where you’ll be greeted with the GraphQL Playground (Figure 4-1). This web app, which comes bundled with Apollo Server, is one of the great benefits of working with GraphQL. From here, you can run GraphQL queries and mutations and see the results. You can also click the Schema tab to access automatically created documentation for the API.
Note
GraphQL Playground has a dark colored default syntax theme. Throughout the book, I’ll be using the “light” theme for its higher contrast. This is configurable in GraphQL Playground’s settings, which can be accessed by clicking the gear icon.
We can now write our query against our GraphQL API. To do so, type the following into the GraphQL Playground:
query
{
hello
}
When you click the Play button, the query should return the following (Figure 4-2):
{
"data"
:
{
"hello"
:
"Hello world!"
}
}
And that’s it! We now have a working GraphQL API that we’ve accessed via the GraphQL Playground. Our API takes a query of hello
and returns the string Hello world!
. More importantly, we now have the structure in place to build a fully featured API.
GraphQL Basics
In the previous section we dove right in and developed our first API, but let’s take a few moments to step back and look at the different pieces of a GraphQL API. The two primary building blocks of a GraphQL API are schemas and resolvers. By understanding these two components, you can apply them more effectively to your API design and development.
Schemas
A schema is a written representation of our data and interactions. By requiring a schema, GraphQL enforces a strict plan for our API. This is because your API can only return data and perform interactions that are defined within the schema.
The fundamental component of GraphQL schemas are object types. In the
previous example we created a GraphQL object type of Query
with a
field of hello
, which returned a scalar type of String
. GraphQL
contains five built-in scalar types:
String
-
A string with UTF-8 character encoding
Boolean
-
A true or false value
Int
-
A 32-bit integer
Float
-
A floating-point value
ID
-
A unique identifier
With these basic components we can construct a schema for an API. We do
so by first defining the type. Let’s imagine that we’re creating an
API for a pizza menu. In doing so, we might define a GraphQL schema type
of Pizza
like so:
type
Pizza
{
}
Now, each pizza has a unique ID, a size (such as small, medium, or
large), a number of slices, and optional toppings. The
Pizza
schema might look something like this:
type
Pizza
{
id
:
ID
size
:
String
slices
:
Int
toppings
:
[
String
]
}
In this schema, some field values are required (such as ID, size, and slices), while others may be optional (such as toppings). We can express that a field must contain a value by using an exclamation mark. Let’s update our schema to represent the required values:
type
Pizza
{
id
:
ID
!
size
:
String
!
slices
:
Int
!
toppings
:
[
String
]
}
In this book, we’ll be writing a basic schema, which will enable us to perform the vast majority of operations found in a common API. If you’d like to explore all of the GraphQL schema options, I’d encourage you to read the GraphQL schema documentation.
Resolvers
The second piece of our GraphQL API will be resolvers. Resolvers perform exactly the action their name implies; they resolve the data that the API user has requested. We will write these resolvers by first defining them in our schema and then implementing the logic within our JavaScript code. Our API will contain two types of resolvers: queries and mutations.
Queries
A query requests specific data from an API, in its desired format. In our hypothetical pizza API we may write a query that will return a full list of pizzas on the menu and another that will return detailed information about a single pizza. The query will then return an object, containing the data that the API user has requested. A query never modifies the data, only accesses it.
Mutations
We use a mutation when we want to modify the data in our API. In our pizza example, we may write a mutation that changes the toppings for a given pizza and another that allows us to adjust the number of slices. Similar to a query, a mutation is also expected to return a result in the form of an object, typically the end result of the performed action.
Adapting Our API
Now that you have a good understanding of the components of GraphQL, let’s adapt our initial API code for our notes application. To begin, we’ll write some code to read and create notes.
The first thing we’ll need is a little bit of data for our API to work
with. Let’s create an array of “note” objects, which we’ll use as the
basic data served by our API. As our project evolves, we’ll replace this
in-memory data representation with a database. For now, we will store
our data in a variable named notes
. Each note in the array will be an
object with three properties, id
, content
, and author
:
let
notes
=
[
{
id
:
'1'
,
content
:
'This is a note'
,
author
:
'Adam Scott'
},
{
id
:
'2'
,
content
:
'This is another note'
,
author
:
'Harlow Everly'
},
{
id
:
'3'
,
content
:
'Oh hey look, another note!'
,
author
:
'Riley Harrison'
}
];
Now that we have some data, we’ll adapt our GraphQL API to work with
it. Let’s begin by focusing on our schema. Our schema is
GraphQL’s representation of our data and how it will be interacted with.
We know that we will have notes, which will be queried and mutated.
These notes will, for now, contain an ID, content, and an author field.
Let’s create a corresponding note type within our typeDefs
GraphQL
schema. This will represent the properties of a note within our API:
type
Note
{
id
:
ID
!
content
:
String
!
author
:
String
!
}
Now, let’s add a query that will allow us to retrieve the list of all
notes. Let’s update the Query
type to include a notes
query, which
will return the array of note objects:
type
Query
{
hello
:
String
!
notes
:
[
Note
!
]
!
}
Now, we can update our resolver code to perform the work of returning
the array of data. Let’s update our Query
code to include the following
notes
resolver, which returns the raw data object:
Query
:
{
hello
:
()
=>
'Hello world!'
,
notes
:
()
=>
notes
},
If we now go to the GraphQL playground, running at
http://localhost:4000/api, we can test the notes
query. To do so, type the following query:
query
{
notes
{
id
content
author
}
}
Then, when you click the Play button, you should see a data
object
returned, which contains the data array (Figure 4-3).
To try out one of the coolest aspects of GraphQL, we can remove any of our requested fields, such as id
or author
. When we do so, the API returns precisely the data that we’ve requested. This allows the client that consumes the data to control the amount of data sent within each request and limit that data to exactly what is required (Figure 4-4).
Now that we can query our full list of notes, let’s write some code that
will allow us to query for a single note. You can imagine the usefulness
of this from a user interface perspective, for displaying a view that
contains a single, specific note. To do so, we’ll want to request a note
with a specific id
value. This will require us to use an argument in
our GraphQL schema. An argument allows the API consumer to pass specific
values to the resolver function, providing the necessary information for
it to resolve. Let’s add a note
query, which will take an
argument of id
, with the type ID
. We’ll update our Query
object
within our typeDefs
to the following, which includes the new note
query:
type
Query
{
hello
:
String
notes
:
[
Note
!
]
!
note
(
id
:
ID
!
)
:
Note
!
}
With our schema updated, we can write a query resolver to return the requested note. To do this, we’ll need to be able to read the API user’s argument values. Helpfully, Apollo Server passes the following useful parameters to our resolver functions:
parent
-
The result of the parent query, which is useful when nesting queries.
args
-
These are the arguments passed by the user in the query.
context
-
Information passed along from the server application to the resolver functions. This could include things such as the current user or database information.
info
We’ll be exploring these further as needed within our code. If
you’re curious, you can learn more about these parameters in
Apollo
Server’s documentation. For now, we’ll need only the information
contained within the second parameter, args
.
The note
query will take the note id
as an argument and find it within our array of note
objects. Add the following to the query resolver code:
note
:
(
parent
,
args
)
=>
{
return
notes
.
find
(
note
=>
note
.
id
===
args
.
id
);
}
The resolver code should now look as follows:
const
resolvers
=
{
Query
:
{
hello
:
()
=>
'Hello world!'
,
notes
:
()
=>
notes
,
note
:
(
parent
,
args
)
=>
{
return
notes
.
find
(
note
=>
note
.
id
===
args
.
id
);
}
}
};
To run our query, let’s go back to our web browser and visit the GraphQL Playground at http://localhost:4000/api. We can now query a note with a specific id
as follows:
query
{
note
(
id
:
"1"
)
{
id
content
author
}
}
When you run this query, you should receive the results of a note with the requested id
value. If you attempt to query for a note that doesn’t exist, you should receive a result with a value of null
. To test this, try changing the id
value to return different results.
Let’s wrap up our initial API code by introducing the ability to create a new note, using a GraphQL mutation. In that mutation, the user will pass in
the note’s content. For now, we’ll hardcode the author of the note.
Let’s begin by updating our typeDefs
schema with a Mutation
type,
which we will call newNote
:
type
Mutation
{
newNote
(
content
:
String
!
)
:
Note
!
}
We’ll now write a mutation resolver, which will take in the note content as an argument, store the note as an object, and add it in memory to our notes
array. To do this, we’ll add a Mutation
object to our resolvers. Within the Mutation
object, we’ll add a function called newNote
, with parent
and args
parameters. Within this function, we’ll take the argument content
and create an object with id
, content
, and author
keys. As you may have noticed, this matches the current schema of a note. We will then push this object to our notes
array and return the object. Returning the object allows the GraphQL mutation to receive a response in the intended format. Go ahead and write this code as follows:
Mutation
:
{
newNote
:
(
parent
,
args
)
=>
{
let
noteValue
=
{
id
:
String
(
notes
.
length
+
1
),
content
:
args
.
content
,
author
:
'Adam Scott'
};
notes
.
push
(
noteValue
);
return
noteValue
;
}
}
Our src/index.js file will now read like so:
const
express
=
require
(
'express'
);
const
{
ApolloServer
,
gql
}
=
require
(
'apollo-server-express'
);
// Run our server on a port specified in our .env file or port 4000
const
port
=
process
.
env
.
PORT
||
4000
;
let
notes
=
[
{
id
:
'1'
,
content
:
'This is a note'
,
author
:
'Adam Scott'
},
{
id
:
'2'
,
content
:
'This is another note'
,
author
:
'Harlow Everly'
},
{
id
:
'3'
,
content
:
'Oh hey look, another note!'
,
author
:
'Riley Harrison'
}
];
// Construct a schema, using GraphQL's schema language
const
typeDefs
=
gql
`
type Note {
id: ID!
content: String!
author: String!
}
type Query {
hello: String
notes: [Note!]!
note(id: ID!): Note!
}
type Mutation {
newNote(content: String!): Note!
}
`
;
// Provide resolver functions for our schema fields
const
resolvers
=
{
Query
:
{
hello
:
()
=>
'Hello world!'
,
notes
:
()
=>
notes
,
note
:
(
parent
,
args
)
=>
{
return
notes
.
find
(
note
=>
note
.
id
===
args
.
id
);
}
},
Mutation
:
{
newNote
:
(
parent
,
args
)
=>
{
let
noteValue
=
{
id
:
String
(
notes
.
length
+
1
),
content
:
args
.
content
,
author
:
'Adam Scott'
};
notes
.
push
(
noteValue
);
return
noteValue
;
}
}
};
const
app
=
express
();
// Apollo Server setup
const
server
=
new
ApolloServer
({
typeDefs
,
resolvers
});
// Apply the Apollo GraphQL middleware and set the path to /api
server
.
applyMiddleware
({
app
,
path
:
'/api'
});
app
.
listen
({
port
},
()
=>
console
.
log
(
`GraphQL Server running at http://localhost:
${
port
}${
server
.
graphqlPath
}
`
)
);
With the schema and resolver updated to accept a mutation, let’s try it
out in GraphQL Playground at http://localhost:4000/api. In the
playground, click the +
sign to create a new tab and write the
mutation as follows:
mutation
{
newNote
(
content
:
"This is a mutant note!"
)
{
content
id
author
}
}
When you click the Play button, you should receive a response containing
the content, ID, and author of our new note. You can also see that the
mutation worked by rerunning the notes
query. To do so, either switch back to the GraphQL Playground tab containing that query, or type the following:
query
{
notes
{
content
id
author
}
}
When this query runs, you should now see four notes, including the recently added one.
Data Storage
We are currently storing our data in memory. This means that anytime we restart our server, we will lose that data. We’ll be persisting our data using a database in the next chapter.
We’ve now successfully implemented our query and mutation resolvers and tested them within the GraphQL Playground user interface.
Conclusion
In this chapter we’ve successfully built a GraphQL API, using the
apollo-server-express
module. We can now run queries and mutations against an in-memory data object. This setup provides us a solid foundation on which to build any API. In the next chapter we’ll explore the ability to persist data by using a database.
Get JavaScript Everywhere now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.