GraphQL concepts I wish someone explained to me a year ago

Part 2: Queries (server implementation)

Naresh Bhatia
Naresh Bhatia
8 min readDec 21, 2018

--

Image by Rostyslav

In part 1 of this series we covered the basics of GraphQL. In this part, we will build a GraphQL server that responds queries that we designed in part 1.

One part will be released every day this week. Follow me or subscribe to my newsletter to get updates on this series.

Source Code

You can download the code for this part by executing the following commands. Skip the first command if you have already cloned the repository.

git clone https://github.com/nareshbhatia/graphql-bookstore.git
cd graphql-bookstore
git checkout 2-queries-server

Test Drive

Before diving into the implementation, let’s look at how the final implementation works. Make sure you have yarn installed on your machine. Now execute the following commands:

cd apollo-bookstore-server
yarn
yarn dev

This will start the server. Point your browser to http://localhost:4000/ — it will show an interactive development environment called GraphQL Playground. The Playground allows you to test GraphQL queries, mutations and subscriptions interactively, without having to build a client. You can also browse your schema by clicking on the Schema button.

Let’s start by performing the authors query. Enter the following query in the left panel and click on the Execute button.

{
authors {
id
name
}
}

You should see the results in the right panel, showing 7 authors that are stored in our data store.

Performing the authors query in Playground

Now try the more complex query from part 1. This should return the author of Clean Code (Robert C. Martin) and all the books he has written.

{
book(id: "clean-code") {
name
authors {
name
books {
name
}
}
}
}

Play around by executing your own queries and become familiar with Playground. It’s a very useful tool when developing GraphQL applications.

Server Implementation

Now that you’ve test-driven the server, let’s dive into the implementation. The Bookstore server uses the Apollo Server library to implement GraphQL functionality. Apollo Server is a popular choice because it does the heavy lifting for us. Here’s the entry point of our server:

# ----- apollo-bookstore-server/src/index.ts -----import { ApolloServer } from 'apollo-server';
import { typeDefs } from './graphql/typedefs';
import { resolvers } from './graphql/resolvers';

const server = new ApolloServer({ typeDefs, resolvers });

server.listen().then(({ url }) => {
console.log(`Server ready at ${url}`);
});

The constructor of ApolloServer takes an options object as its parameter. We’re passing two options into it:

  • typedefs: This is the full GraphQL schema assembled from smaller type definitions stored in multiple modules.
  • resolvers: These are helper functions that assist the Apollo Server to resolve queries, mutations and subscriptions. More about these later.

After constructing the server object, we simply call its listen() method. This tells the server to listen for GraphQL requests. That’s all there is to the server. Of course, we still need to understand the details behind the typeDefs and the resolvers.

Side note
Normally we would also pass our data sources into the options object. This allows Apollo Server to pass the data sources to the resolvers as context. Unfortunately, at the time of this writing, the Apollo Server fails to do this in case of subscription workflows. Hence I decided not to use this feature for now. Instead I am keeping data sources in a global object that the resolvers can access. You can read more about this issue here.

Assembling the schema

In part 1, we defined the bookstore schema in three modules: author.graphql, publisher.graphql and book.graphql. Now we need to stitch together these modules into a single schema so we can hand it to the Apollo Server. We’ll use a library called merge-graphql-schemas to do this (there are several open-source solutions available — this one is super simple and popular). See the code below:

# ----- graphql/typedefs.ts -----import { fileLoader, mergeTypes } from 'merge-graphql-schemas';
import * as path from 'path';

const typesArray = fileLoader(path.join(__dirname, './typedefs'));

export const typeDefs = mergeTypes(typesArray, { all: true });

We use the fileLoader function to import all files from the typedefs folder. Then we merge the types into a single exported variable called typeDefs.

Implementing Resolvers

Remember, the Apollo server needs resolver functions to resolve queries, mutations and subscriptions. Let’s understand what these functions are.

When the server receives a query, it calls the resolver function specifically responsible for that query. The resolver function has the ability to fetch data from one or more data sources to satisfy the query. The returned data may include one or more nodes from the universal object graph. However, depending on the query, the server will need to dig deeper into these objects and collect their fields and related objects. Each of these deeper questions needs a dedicated resolver. The server will call these resolvers in a breadth-first search manner, following the structure of the query.

Let’s understand this mechanism by using the book query shown above. Here’s the series of questions the server must ask:

Give me the book with id="clean-code"
Give me the book's name
Give me the book's authors
Give me the author's name
Give me all the books the author has written
Give me the book's name

Side note
As you can see, each field in the GraphQL schema must be backed by a resolver. This includes not only the fields of our domain entities, but also the queries — after all, queries are also the fields of the root Query type. The resolver’s job is to fetch the requested data from data sources and return it to the server.

Before we can start writing resolvers, we need to understand what a resolver function looks like. Resolver functions accept four arguments and return data:

fieldName: (parent, args, context, info) => data

Let’s use the following query to understand what gets passed into each argument:

{
author(id: "eric-evans") {
name
}
}
  • parent: Recall, the GraphQL server performs a breadth-first traversal to create the query response. At each level it calls a resolver. The parent argument of the resolver is simply the result returned by the resolver at the previous level. The call to the root level resolver receives a null in this argument. For the query above, the author resolver receives a null as the parent argument whereas the name resolver receives the result of the author resolver.
  • args: An object containing the arguments passed to the field. For the query above, the author resolver will receive { id: “eric-evans" } as the args argument whereas the name resolver will receive a blank object.
  • context: An object shared by all resolvers in a GraphQL operation. It can be used to pass any information that all resolvers need, e.g. per-request state (such as authentication information) or reference to data sources.
  • info: An abstract syntax tree (AST) representation of the query or mutation — used only in advanced use cases.

Armed with this knowledge, we should be able to write our resolvers. Again, in the interest of modularity, we will create a separate file for each entity’s resolvers. Let’s start with author resolvers. We will let the author typeDefs guide what resolvers we need to write. Remember that we need one resolver for every field. For reference, here are the author type definitions from part 1:

type Query {
authors: [Author!]!
author(id: ID!): Author!
}
type Author {
id: ID!
name: String!
books: [Book!]!
}

Based on these type definitions, below is the code for author resolvers:

# ----- graphql/resolvers/author-resolvers.ts -----import { dataSources } from '../../datasources';

export default {
Query: {
authors() {
return dataSources.bookService.getAuthors();
},

author(parent, args) {
return dataSources.bookService.getAuthor(args.id);
}
},

Author: {
id(parent, args) {
return parent.id;
},

name(parent, args) {
return parent.name;
},
books(parent) {
return dataSources
.bookService.getAuthorBooks(parent.id);
}
}
};

Starting at the very top, we import dataSources. This contains references to services that allow us to fetch data. It is a best practice not to pollute the resolvers with low-level data fetching code. Hence all the fetching code has been separated out into its own layer. This approach allows us to switch database technologies without affecting the rest of the server. Moreover, it keeps the resolvers light-weight — essentially as mediators between the server framework and the data sources.

Side note
The recommended way to access data sources is to pass them in the resolver’s context argument. However due to the open issue with subscription workflows, I have chosen not to do so.

The bookstore application has only one data source. Hence our dataSources object contains only one service — the bookService.

import { BookService } from './book-service';

export const dataSources = {
bookService: new BookService()
};

In the interest of simplicity, BookService implements an in-memory database. In a real application, this can be replaced by a more robust data solution such as MySQL, Postgres or MongoDB. The important part is that BookService methods return promises, so switching to a different data source should be easy.

Now that we understand the dataSources import, let’s focus on individual field resolvers. Again, there is a one-to-one mapping between fields and resolvers. Let’s start with the resolver for the authors field on type Query:

authors() {
return dataSources.bookService.getAuthors();
}

This resolver simply fetches all the authors by calling the bookService and returns them. More formally, it returns a promise that resolves to an array of Authors.

Next let’s look at the resolver for the author query. This query receives an author id as the argument.

author(parent, args) {
return dataSources.bookService.getAuthor(args.id);
}

Here we use the id argument sent to the query to get the author. The bookService returns the full author object, e.g. { id: "eric-evans", name: "Eric Evans"}. This is important for the next few resolvers we will review.

Now let’s look at the resolver for the id field of an author:

id(parent, args) {
return parent.id;
}

This is an easy one — it leverages the fact that the parent resolver in this case is the author resolver which returns the full Author object (see author resolver above). We simply pick up the id value from the parent argument and return it. In fact this is so easy that you can omit this resolver and the Apollo Server can figure it out. That’s why I have omitted it in the code base!

Ditto for the name resolver.

Finally, let’s look at the books resolver. This is an interesting one. books is not a property of the Author object. It’s a relation to books that the author has written. That’s why the author resolver did not return it. For this resolver we will have to send the author id to our data source and fetch the related books. That’s exactly what the code does:

books(parent) {
return dataSources
.bookService.getAuthorBooks(parent.id);
}

I hope this gives you a good feel of how to write resolvers. Now look at the publisher and book resolvers and see if they make sense.

Summary

We have now covered the full server implementation for queries. This is what we’ve learned:

  • How to you use GraphQL Playground to send queries to the server
  • How to create and start and Apollo Server
  • How to merge type definitions into one cohesive schema
  • How to write resolvers

Are you enjoying the series so far? I would love to get your questions and comments.

In part 3, we will implement a React client to go with our server.

Read Part 3: Queries (client implementation) >

Resources

  • GraphQL Server Basics on the Prisma blog: Excellent explanation of GraphQL schemas, type definitions and resolvers — a must read.

--

--