DEV Community

Engin Arslan
Engin Arslan

Posted on • Originally published at awesomecoding.co

Learn How to Build a GraphQL API in Node.js Using Apollo Server

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
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

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
  }
]
Enter fullscreen mode Exit fullscreen mode

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"]
  }
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

We will also create an index.js file at the root of this project folder.

touch index.js
Enter fullscreen mode Exit fullscreen mode

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"
},
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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. (Like 3.14)
  • String: Represents textual data.
  • Boolean: Represents boolean data (Like true or false)
  • ID: Represents a unique identifier. GraphQL clients can use this ID for caching / performance optimization purposes. It is recommended that you don’t have this ID 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 the id 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!]!
}
Enter fullscreen mode Exit fullscreen mode

We are using several new types in the Product type definition for the following fields:

  • inventory: Int is used for the inventory field since the product inventory is defined using whole numbers.
  • collections: We are defining an array of Collection types as the return type of the collections 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 the price 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!
  }
Enter fullscreen mode Exit fullscreen mode

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!]!
}
Enter fullscreen mode Exit fullscreen mode

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!]!
}
Enter fullscreen mode Exit fullscreen mode

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!]!
  }
`;
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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.

https://i.imgur.com/up3cWDI.png

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.

https://i.imgur.com/8ZIzLHr.png

Let’s run a query against our GraphQL API. Here is a query that would get the name of all the products

{
  products {
    name
  }
}
Enter fullscreen mode Exit fullscreen mode

The result would be:

{
  "data": {
    "products": null
  }
}
Enter fullscreen mode Exit fullscreen mode

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!]
}
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
        });
      });
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

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;
      });
    });
  },
},
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

We need to define the parameters that we will pass into this query inside the variables section.

{
  "id": "random-id-00"
}
Enter fullscreen mode Exit fullscreen mode

This is how that screen looks like.

https://i.imgur.com/6FmV6Y7.png

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;
      });
    },
  },
Enter fullscreen mode Exit fullscreen mode

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)