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.

A screenshot of the GraphQL Playground
Figure 4-1. The GraphQL Playground
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!"
  }
}
The hello query in the GraphQL Playground
Figure 4-2. The hello query

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).

The notes query in the GraphQL Playground
Figure 4-3. The notes query

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).

A note query with only content data requested in the GraphQL Playground
Figure 4-4. A notes query with only content data requested

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

Information about the query itself.

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.