You might want to build an API to enable external applications like desktop or mobile clients to communicate with your services.
When building a Web API, you can choose from two popular options. These are REST and GraphQL APIs. Which option you decide to choose depends on various factors. I have previously written about the differences between REST and GraphQL APIs. This post will show how to build a GraphQL API in Node.js using Apollo Server.
You can find the working code for this post at this Codesandbox:
Apollo Server
Apollo Server is an open-source GraphQL server compatible with any GraphQL client. It is a pretty reliable choice for implementing a GraphQL server on your Node.js backend. It is easy to get started and rich with additional features if you want to customize it for your own needs.
GraphQL Schema
One of the best aspects of working with a GraphQL API is the flexibility that it provides on the client-side. When using a GraphQL API, clients can tailor their own custom queries to submit to the backend. This is a major departure from how the REST APIs work.
This is what a GraphQL query might look like:
{
books {
title
author {
name
books {
title
}
}
}
}
Here we have a query that is for fetching all the books alongside their title and authors, getting the name of all those authors and all the books that those specific authors have written. This is a deeply nested query, and we could keep nesting it as well!
When we allow the clients to craft their own queries, they are empowered to fetch the exact amount of data they require. A mobile application can be built to query for fewer fields, whereas a desktop application can query for a lot more.
But how does a client know which data to request from the server? This is made possible by something called a schema.
GraphQL servers use a definition file called a schema to describe the existing types present in the backend so that the client application can know how they can interact with the API.
Schemas in Apollo Server
One of the major differentiators between GraphQL servers is how they require the schema to be implemented. Apollo Server requires the schema to be implemented using the spec-compliant human-readable schema definition language (SDL). Here is what SDL looks like:
type Book {
title: String
}
type Author {
name: String
books: [Book]
}
As you can see, it is fairly easy to understand what types exist and what attributes (or fields) these types have by just looking at this schema written using SDL.
You might have seen other GraphQL server solutions where the schema is implemented by using a more programmatic approach. Here is an example of how schemas are implemented using the express-graphql
library. (link: https://github.com/graphql/express-graphql)
new GraphQLObjectType({
name: 'Book',
fields: {
title: {
type: GraphQLString,
// define a resolver here
},
},
});
These different approaches present a certain kind of tradeoff. SDL makes it easy for anyone to understand what is happening in the schema, while it might be harder to maintain when your schema becomes very large. When schema is programmatic, it might be easier to modularize, customize and scale the schema, but the readability can suffer.
Getting Started
Let’s create some mock data to explore building APIs using Apollo Server. For this example, we will be building a GraphQL API for an online store that has a bunch of products and collections that include those products. Our API should be able to fetch and update these products and collections.
We will have two files called products and collections to contain this data.
collections.json
[
{
"id": "c-01",
"title": "Staff Favorites",
"description": "Our staff favorites",
"isPublished": true
},
{
"id": "c-02",
"title": "Best Selling",
"description": "These are selling out fast!",
"isPublished": true
},
{
"id": "c-03",
"title": "In Season",
"description": "Discover what is in season",
"isPublished": true
}
]
products.json
[
{
"id": "random-id-00",
"category": "apparel",
"name": "The Best T-Shirt",
"brand": "A&A",
"inventory": 32,
"price": {
"amount": 100,
"currency": "USD"
},
"collections": ["c-01"]
},
{
"id": "random-id-01",
"category": "stationery",
"name": "The Best Pencil Case",
"brand": "Pencils Forever",
"inventory": 5,
"price": {
"amount": 25,
"currency": "USD"
},
"collections": ["c-02", "c-03"]
}
]
We have three collections and two products. This is enough to get started.
Setting up Apollo Server
You will need to be comfortable using JavaScript and have a recent version of Node.js (12+) to follow this introduction.
Let’s create a new folder and run npm init -y
in this folder. This will create a package.json file that will keep a record of the project's dependencies. Initially, we will be installing apollo-server and graphql libraries.
npm install --save apollo-server@^3.5.0 graphql@^16.2.0
We will also install a library called nodemon that will automatically restart the server whenever there is a change. This will help us see the results of our updates much faster. This dependency has to do with the development environment, so we will install it using the --save-dev
flag.
npm install --save-dev nodemon@2.0
We will also create an index.js file at the root of this project folder.
touch index.js
We will add a start script in our package.json file to call nodemon with our index.js file.
"scripts": {
"start": "nodemon index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
Let’s create a folder called data
and place the collections.json
and products.json
files into that folder.
We can now start setting up our server in this index.js file.
const { ApolloServer } = require("apollo-server");
const server = new ApolloServer();
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
We have imported the ApolloServer from the apollo-server package and trying to run it by calling its listen
method. We can run this file by calling our start script.
npm start
At this point, we would get an error since ApolloServer requires you to have type definitions (schema) and a resolver object on instantiation. We already know what a schema is. A resolver object is an object that has a bunch of resolver functions. A Resolver function is a function that specifies what data should a single GraphQL field return on a query. We don’t have a schema or resolvers, so nothing works.
Let’s start by creating a schema.
Creating a Schema and GraphQL Types
First, we will import the gql
function and then create a typeDefs
variable to pass into the ApolloServer
.
const { ApolloServer, gql } = require("apollo-server");
const typeDefs = gql``;
const server = new ApolloServer({
typeDefs,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
We can now start declaring types for our GraphQL API inside the backticks for the gql
function.
Remember the shape of our data for collections and products. We will start by creating the type definition for a collection.
type Collection {
id: ID!
title: String!
description: String
isPublished: Boolean!
}
This is a type definition for a collection object. Notice how readable it is. Our object has three properties, and we have created a corresponding type with three fields. Note that there doesn’t need to be a one-to-one mapping in between a data object and the corresponding type. The GraphQL type represents an interface for a user (client) to interact with. The client might or might not care about the underlying shape of the data. We should make sure to only surface information that the client would care about in a way that is easy to understand.
Int
, Float
, String
, Boolean
, and ID
are the most basic types we can use when defining types in GraphQL.
-
Int
: Represents whole numbers. -
Float
: Represents fractional numbers. (Like3.14
) -
String
: Represents textual data. -
Boolean
: Represents boolean data (Liketrue
orfalse
) -
ID
: Represents a unique identifier. GraphQL clients can use this ID for caching / performance optimization purposes. It is recommended that you don’t have thisID
field be human-readable so that the clients wouldn’t be inclined to implement a logic on their side that relies on a pattern that might surface in the ID. In our example, we will leave theid
fields to be human-readable, though.
We use String
, Boolean
, and ID
types in our example for collections. Another thing to note is that the usage of the bang symbol (!
). !
indicates that the field can not be null (empty). It has to have value.
Let’s create the type definition for a product.
type Product {
id: ID!
category: String!
name: String!
brand: String
inventory: Int!
price: Price
collections: [Collection!]!
}
We are using several new types in the Product type definition for the following fields:
-
inventory:
Int
is used for theinventory
field since the product inventory is defined using whole numbers. -
collections: We are defining an array of
Collection
types as the return type of thecollections
field. The!
usage here suggests that the array can not contain a null value, and the field can not be equal to a null value. So the value can only be an empty array or an array with collection objects inside. -
price: Here, we define a new object type called
Price
for theprice
field. An object type is a type that includes fields of its own. The definition of that object type will be as follows.
type Price {
amount: Int!
currency: String!
}
There is an enhancement we can make on the Product type. Notice how the category
field is defined as a String
. The categories in online stores tend to be equivalent to specific values like apparel
, accessories
, stationery
, etc. So instead of defining the category
**field to be any string, we can define it so that it would only be equivalent to certain values. The way to do that would be using an **enum type. Enum types are useful when defining a set of predefined values for the given field. Let’s create an **enum** type that has three category values.
enum Category {
apparel
accessories
stationery
}
type Product {
id: ID!
category: Category!
name: String!
brand: String
inventory: Int!
price: Price
collections: [Collection!]!
}
We are almost done with creating our schema! Finally, we need to define a special object type called Query that defines all the top/root-level queries we can run against our GraphQL API.
type Query {
collections: [Collection!]!
products: [Product!]!
}
Here is what the entire schema looks like at this point.
const typeDefs = gql`
type Collection {
id: ID!
title: String!
description: String
isPublished: Boolean!
}
type Price {
amount: Int!
currency: String!
}
enum Category {
apparel
accessories
stationery
}
type Product {
id: ID!
category: Category!
name: String!
brand: String
inventory: Int!
price: Price
collections: [Collection!]!
}
type Query {
collections: [Collection!]!
products: [Product!]!
}
`;
We can now pass this schema into our ApolloServer and have things start working!
const server = new ApolloServer({
typeDefs,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
If we are to visit http://localhost:4000/
or wherever the API is hosted locally, we would land on an Apollo branded welcome page. Let’s click on the big button that reads Query Your Server.
Clicking on that button will take us to a GraphQL explorer interface. Using this interface, we can run GraphQL queries against our API. We can also explore the documentation of our API. Note that we didn’t explicitly write any documentation when building our API. It gets generated automatically using the data already available in the schema. That is a pretty awesome feature of GraphQL! This means that our documentation will always be up to date with our code.
Let’s run a query against our GraphQL API. Here is a query that would get the name of all the products
{
products {
name
}
}
The result would be:
{
"data": {
"products": null
}
}
We are getting null
as a result since we didn’t define any resolvers that would specify what this field should return when queried. Under the hood, Apollo Server has created a default resolver that is returning a null
result since this is a nullable field.
If we defined the Query object so that the products are not nullable then we should ideally receive an empty list as a result.
type Query {
collections: [Collection!]
products: [Product!]
}
However,, Apollo Server default resolver doesn’t take care of that situation, so we receive an error.
Creating Resolvers
A resolver is a function that defines what data a single field should return when queried.
The Query
type has two fields called collections
and products
. Let’s create very simple resolvers for these fields that will return an empty array. We will provide this resolvers object (that contains the resolver functions) inside the ApolloServer function.
const resolvers = {
Query: {
collections: () => {
return [];
},
products: () => {
return [];
},
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
Now, if we are to run our previous query, we would get an empty array instead. The resolver function we have defined for products
specifies how that query should be resolved.
{
products {
name
}
}
Let’s create a proper resolver for these fields. We will first import the collections
and products
data into index.js
. Then we will return this data from these queries instead of just returning an empty array. Here is what the implementation looks like.
const { ApolloServer, gql } = require("apollo-server");
const collectionsData = require("./data/collections.json");
const productsData = require("./data/products.json");
const typeDefs = gql`
type Collection {
id: ID!
title: String!
description: String
isPublished: Boolean!
}
type Price {
amount: Int!
currency: String!
}
enum Category {
apparel
accessories
stationery
}
type Product {
id: ID!
category: Category!
name: String!
brand: String
inventory: Int!
price: Price
collections: [Collection!]!
}
type Query {
collections: [Collection!]
products: [Product!]
}
`;
const resolvers = {
Query: {
collections: () => {
return collectionsData;
},
products: () => {
return productsData;
},
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
Now that we have defined the resolvers for the collections
and products
, we can query these fields for the data they represent. As I have mentioned at the beginning of this article, one of the strengths of GraphQL is the ability for the clients to create their own queries. We can even write a query that would ask for data from these two fields at the same time! This wouldn’t be possible to do in a REST API.
{
collections {
title
}
products {
category
name
brand
inventory
price {
amount
currency
}
}
}
We are not including the collections
field for the products
in the above GraphQL query. That is because our existing resolver functions currently don’t know how to return the data for that particular field. If we tried to query that field, we would receive an error.
To fix this problem, we need to create another resolver function for the collections
field of the Product
type. This resolver function will need to make use of the resolver arguments.
const resolvers = {
Query: {
collections: () => {
return collectionsData;
},
products: () => {
return productsData;
},
},
Product: {
collections: (parent, args, context, info) => {
const { collections } = parent;
return collections.map((collectionId) => {
return collectionsData.find((collection) => {
return collection.id === collectionId;
});
});
},
},
};
Resolver Arguments
Any resolver function receives four arguments. These arguments are conventionally called parent
, args
, context
, and info
. Of course, you could choose different names for these arguments depending on your purposes.
For now, we will only take a look at the first two arguments.
parent
This argument refers to the return value of the resolver for the field’s parent. In our example, the parent of the field collections
is a product
. So this value would be equivalent to a product item.
args
We could have fields that accepts arguments (a parametrized field). The args argument captures the arguments provided by the client to query a parametrized field. We will look into this use case in a bit. For now, we only care about the parent
argument.
Our resolver function for the collections
field uses the parent
argument to fetch the collections
array of the parent product. We use the id
data in this array to find and return the collection objects from the collectionsData
.
Product: {
collections: (parent, args, context, info) => {
const { collections } = parent;
return collections.map((collectionId) => {
return collectionsData.find((collection) => {
return collection.id === collectionId;
});
});
},
},
Now, if we are to run a query that fetches fields of the collections
field, we would be able to get the collection objects that are associated with each product.
{
collections {
title
}
products {
category
name
brand
inventory
price {
amount
currency
}
collections {
id
title
}
}
}
Fields with Arguments
As mentioned earlier, we can define fields that would accept arguments in our schema. Let’s create a new field under Query
type called productById
that would get the product of a given ID. Here is what that would look like in our schema.
type Query {
collections: [Collection!]
products: [Product!]
productById(id: ID!): Product
}
productById
is a field that accepts an id
argument and returns the product type that has the given id if it exists. Notice the return type for the field doesn’t have the !
symbol. This means that the returned value can be of type Product
or null
. That is because a product of a given id might not exist.
Let’s query this field using the GraphQL API Explorer.
query($id: ID!) {
productById(id: $id) {
name
}
}
We need to define the parameters that we will pass into this query inside the variables section.
{
"id": "random-id-00"
}
This is how that screen looks like.
We would be getting a null
as a result of this query since we didn’t implement the resolver function for this field. Let’s do that.
We will be adding a new resolver function under Query
called productById
. It is going to fetch the given id from the provided args
parameter and return the product with the matching id.
Query: {
collections: () => {
return collectionsData;
},
products: () => {
return productsData;
},
productById: (_parent, args, _context, _info) => {
const { id } = args;
return productsData.find((product) => {
return product.id === id;
});
},
},
Notice the underscore (_
) before the argument names that we are not making use of in our function. This is a coding convention to indicate that a named argument to a function is not being used. Now, our previous query should work and return the desired product!
There is a lot more to GraphQL then what I wrote about here but this should be a decent introduction to the subject. In production, we wouldn’t have any hardcoded product or category data in our servers as we did here. We would rather fetch this data from a database or from some other API. When working with data, you might want to use classes called data sources that manages how you interact with that data and helps with things like caching, deduplication, etc. You can learn more about data sources here.
If you wanted to deploy this API, you can use cloud services such as Heroku, Google Cloud, etc. More information on the deployment process can also be found in the Apollo Server documentation.
You can also see the full code in action at Codesandbox!
Top comments (0)