Adding GraphQL to a RESTful API

Arnaud Dostes
11 min readFeb 8, 2020

--

June 2021 update: while some of the content of this article remains accurate (and hopefully useful), it is a bit of a contrived example. It assumes that there is an existing REST api running on Express that needs to be maintained, while also exposing a GraphQL endpoint. In reality, a better solution would be to keep the REST api as is, and spin up another GraphQL endpoint as a separate server without mixing the two, there is no need to have the same server handle both REST and GraphQL queries, separation of concerns is I think a good thing. Then use a RESTDataSource to query the REST endpoint from the GraphQL server.

In this article, we will build a simple REST api with Express. We will see its limitations and how we can improve it with GraphQL. At the end, we will have an Express app that exposes data that can be queried using a RESTful API or GraphQL.

A live demo is available here

The code is hosted on github

Building a REST api

Let’s start by creating a new project:

$ mkdir express-graphql
$ cd express-graphql
$ npm init -y

We want to use Express to serve our apis, so let’s install it as well, and let’s create a source directory while we’re at it.

$ npm install express
$ mkdir src

Our app is going to be dead simple at first, and we’ll add to it as we go along. For now, we just want to create a simple express server:

// src/app.jsconst express = require('express');
const app = express();
const port = process.env.PORT || 3000;app.listen(port, () => {
console.log(`🏃‍♂️ on port ${port}`);
});

OK, so this will run but it won’t do much. The next step is to actually return some data.

For this example, we’re going to use Books and Authors:

  • A Book has an Id and a Title
  • An Author has an Id and a Name
  • An Author can have written many books
  • A Book has one and only one author.

We want to be able to use our REST API to browse books and authors, and get books written by an author, and the author of a book. Our endpoints will look like this:

  • Get all authors/api/authors
  • Get one author /api/authors/:authorId
  • Get all the books an author has written /api/authors/:authorId/books
  • Get all books /api/books
  • Get one book /api/books/:bookId
  • Get the author of a book /api/books/:bookId/author

To create the routes, we’ll use Express.Router:

// REST route for authors
const authorRouter = express.Router();
app.use('/api/authors', authorRouter);
// REST route for books
const bookRouter = express.Router();
app.use('/api/books', bookRouter);

Lucky for us, we have a datastore that exposes the following methods:

  • getAuthors(): return a list of authors
  • getAuthor(authorId: String): return a single author
  • getAuthorBooks(authorId: String): return all the books for an author
  • getBooks(): return a list of books
  • getBook(bookId: String): return a book
  • getBookAuthor(bookId: String): return the author of a book

A small aside here: the implementation details of the store do not matter in this context, what is important is the methods we have to access the data. In our example, we’re simply using JS arrays. Also, the authorId and bookId are strings, but historically, especially in SQL databases, we’re used to seeing integers used for identifiers. However, in noSQL databases, and even increasingly with SQL databases, it’s common to use a UUID as a key, and that’s a string. GraphQL serializes identifiers as strings and this will come in handy later. Finally, request parameters in Express are always strings. So for the sake of simplicity, we’ll manipulate all identifiers as strings, but you could make it work with integers.

We have our Express server, we have our /api/authors and/api/books routes, let’s now map our routes with our datastore:

First, the authors:

// return a list of authors
authorRouter.route('/').get((req, res) => res.json(db.getAuthors()));
// return an author by authorId
authorRouter
.route('/:authorId')
.get((req, res) => res.json(db.getAuthor(req.params.authorId)));
// return a list of books for an author
authorRouter
.route('/:authorId/books')
.get((req, res) => res.json(db.getAuthorBooks(req.params.authorId)));

Next, the books:

// a list of books
bookRouter.route('/').get((req, res) => res.json(db.getBooks()));
// a book by bookId
bookRouter
.route('/:bookId')
.get((req, res) => res.json(db.getBook(req.params.bookId)));
// the author of a book
bookRouter
.route('/:bookId/author')
.get((req, res) => res.json(db.getBookAuthor(req.params.bookId)));

And that’s it, the REST api portion of the app is complete!

Feel free to lift this sample datastore and include it in your file:

const db = require('./db');

Let’s test each endpoint:

http://localhost:3000/api/authors
http://localhost:3000/api/authors/1
http://localhost:3000/api/authors/1/books
http://localhost:3000/api/books/
http://localhost:3000/api/books/20
http://localhost:3000/api/books/20/author

Now this is great and does the job, and it looks like many a REST api. However, it comes with the same problems. For example, it is not possible to get a list of authors and books or books and authors without making two service calls. Two calls are also needed to get a single author and the books they’ve written or a single book and its author. So it’s very likely, however the frontend is built, that we’ll be making multiple calls as soon as we start drilling down into author’s books or a book’s author, and that’s painful and difficult for frontend developers, it requires some thinking in terms of organizing, storing, caching and presenting the data depending on how the information will be laid out, even for such a simple data structure as the one we’re using here, so imagine in much larger and complicated applications!

Have you ever had to deal with an ad hoc service that solely exists to solve a specific problem like performance or structure, or multiple endpoints that kind of look the same, and kind of return the same data, that are similar enough that they’re kind of the same and could be grouped as one, but different enough to be kept separate? We’ve all been there.

Wouldn’t it be nice if the frontend got to decide what data is returned, and what it should look like?

GraphQL was created to solve all these issues! Why should the backend services dictate what data is returned and how it is shaped when it is the frontend that needs it!

So let’s make our Express app GraphQL aware, and see how to use it to our advantage.

It is possible to add GraphQL to an existing Express app and to use REST and GraphQL side by side. This can come in handy when needing to support legacy applications while offering newer applications the option to use GraphQL.

Adding GraphQL capabilities to our application

We’re going to use Apollo Server in our existing application. This solution offers an integration library that we can use:

$ npm install apollo-server-express

Next we’re going to import it in our project:

const { ApolloServer, gql } = require('apollo-server-express');

GraphQL requires us to do a bit of work up front. We need to define type definitions and resolvers. The type definitions describe the data, and the resolvers how to find it.

Let’s revisit our data: we know we have books and authors, an author has a name, and a book has a title. Let’s not think about the relationship between the two just yet. We can define the type definitions using the gql function we just required above.

const typeDefs = /* GraphQL */ gql`
type Author {
id: ID!
name: String
}
type Book {
id: ID!
title: String
}
`;

This syntax might look unfamiliar, so let’s break it down. We’re using what’s call a tagged template. Don’t worry too much about how it works, to define a type definition, use gql followed by a string between back ticks ` . The /* GraphQL */ comment right before it is a way to tell an editor that understands GraphQL (like vscode) that what is in the next instruction is GraphQL and can be formatted as such. This way the code is kept nice and tidy and formatted, even when it technically is a string and between back ticks. Also, if a mistake is made, the code won’t format and the error will be highlighted, it’s very convenient! Each type block corresponds to an object in our data, here Author and Book, with their respective attributes and their type, name for Author and title for Book. The title and name are strings : String and the id is well… an ID . These are called Scalar types, sort of like the leaf of a tree, or a primitive. Note, ID is always resolved as a string, which is one of the reasons why we decided to use strings for identifiers. Last thing, ID is followed by ! , that just means it’s mandatory, which makes sense as it’s a key in our database.

Next we want to query books and authors, we want to be able to get either a list or a single element. So let’s add a new type to our type definitions, a Query :

const typeDefs = /* GraphQL */ gql`type Query {
Authors: [Author]
Author(id: ID!): Author
Books: [Book]
Book(id: ID!): Book
}
type Author {
id: ID!
name: String
}
type Book {
id: ID!
title: String
}
`;

We’ve defined 4 queries:

- Authors which returns a list of objects of type Author ([Author] ). The [] indicate that it is a list.

Author(id: ID!) which takes in an id of type ID! and returns an Author

Books which returns a list of Books [Books]

Book(id: ID!) which takes in id and returns [Book]

Again, we use the ! to explicitly define the input parameter as mandatory.

But what about the relationship between authors and books? Wouldn’t it be nice to be able to fetch books and authors at the same time? So let’s extend Author so it has a list of books ([Book]), and extend Book so it has an Author.

const typeDefs = /* GraphQL */ gql`
type Query {
Authors: [Author]
Author(id: ID!): Author
Books: [Book]
Book(id: ID!): Book
}
type Author {
id: ID!
name: String
}
type Book {
id: ID!
title: String
}
extend type Author {
books: [Book]
}
extend type Book {
author: Author
}

`;

Using the extend keyword allows us to establish this relationship without changing the type of the objects.

Now that this is done, the next step is to write the resolvers.

A resolver is a simple object that returns a Query object. The Query object will expose a function that will be mapped to each query in our type definition. We’re going to make 4 queries.

  • Authors: [Author] :Authors() returns an array of Author
  • Author(id: ID!): Author : Author(authorId:String) return an Author
  • Books: [Book] : Books() returns an array of Book
  • Book(id: ID!): Book : Book(bookId: String) returns a book

The Query object will then look like this:

const resolvers = {
Query: {
Authors() {
return db.getAuthors();
},
Author(_, { id }) {
return db.getAuthor(id);
},
Books() {
return db.getBooks();
},
Book(_, { id }) {
return db.getBook(id);
},
},
};

Each of these functions actually receives four parameters but here we’ll look only at the second one (args) which is the arguments received from the client. Here we want the id.

Our resolver is still missing something. In the type definitions, we’ve described an Author as having an id and a name, and a Book as having an id and a title, then we extended Author to have a list of Books, and Book to have an Author. Let’s add to our resolvers object a way to resolve this relationship. The Author was extended to have a book attribute, so let’s define it in the resolver:

const resolvers = {
Query: {
// ...
},
Author: {
books(author) {
return db.getAuthorBooks(author.id);
},
},

};

The books attribute takes in an author as the first parameter. We then call the database like we did in the REST part of the application. Where does this argument come from? Well, just like the Query type, the books function receives four arguments, the first one is the parent. In our type definition, we extended Author to have books, so the parent object will be the Author for that Book. It’s a graph after all! Since we have the author, we can query the database to return all the books for that author.

Now the same thing with Book and author. The parent of author is the book, so we can use that id to query the database.

Book: {
author(book) {
return db.getAuthor(book.authorId);
},
},

The completed resolvers should look like this:

const resolvers = {
Query: {
Authors() {
return db.getAuthors();
},
Author(_, { id }) {
return db.getAuthor(id);
},
Books() {
return db.getBooks();
},
Book(_, { id }) {
return db.getBook(id);
},
},
Author: {
books(author) {
return db.getAuthorBooks(author.id);
},
},
Book: {
author(book) {
return db.getAuthor(book.authorId);
},
},
};

Finally let’s connect our Apollo Server with our existing application:

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

And restart!

Apollo Server exposes a playground to test GraphQL queries. Don’t worry, it’s only available in development, Apollo is smart enough to know the difference (it uses process.env.NODE_ENV). So open http://localhost:3000/graphql to access the playground.

Paste the following queries in the left pane, and click the big Play button:

query {
Authors {
name
}
Author(id: 2) {
name
}
}

Here we’re executing two queries, Authors which return the list of authors, and Author, which returns an author with id 2. Note that we’re only requesting the name, so that’s all we’ll get, if we want the id, we have to specify it as well:

query {
Authors {
id
name
}
Author(id: 2) {
id
name
}
}

That’s it, no more over or under fetching data, we can get just what we need!

It would be nice to be able to get the books an author has written, so let’s just add it to the query. Notice that we can’t just say books , we have to specify which attributes we want.

query {
Authors {
name
books {
title
}

}
Author(id: 2) {
name
books {
title
}

}
}

Same as previously, if we want more info on the books, we can just add id to the block under books .

The queries for Books are the same, and we can run as many queries as we want, even all at once:

query {
Authors {
id
name
books {
title
}
}
Author(id: 2) {
id
name
}
Books {
id
title
}
Book(id: 10) {
id
title
author {
name
}
}
}

If we go back to our initial problem with REST, and we want to get all the books for a specific author, it becomes super easy, and it can be done in only one call, returning exactly what we need.

A word of caution

Be mindful of what’s happening in the backend. Every time we are getting the books for for an author, or the author of a book, we’re making two calls to the database: the first one to get the Author(s)/Book(s) and then a subsequent one for the associated books or author. This can quickly lead to performance degradation: if we have n authors/books, the back end will make n+1 queries to the database. It is also possible to get books for an author, then for each author its list of books, and for each book its authors, etc. It’s a graph! So don’t do that. The front-end should only query what it needs, no more no less, and be mindful of processing time.

You can turn on tracing to see how long each operation takes:

const server = new ApolloServer({
resolvers,
typeDefs,
tracing: (process.env.NODE_ENV === 'development'), // tracing
});

There are many more exciting things to do with GraphQL, like Mutations (modify data, what would be done using POST/PUT/PATCH/DELETE in Rest) and Federation (aggregate multiple endpoints in one queryable structure) come to mind.

Next Steps

Feel free to explore the code: https://github.com/arnaudNYC/express-graphql

And the application itself: https://codesandbox.io/s/express-graphql-cvwxh

In this article, we created four queries:

  • Authors()
  • Author(id: ID!)
  • Books()
  • Book(id:ID!)

Try simplifying the code to only have two queries: Authors(id:ID) and Books(id:ID) where id is optional and is used as a filter. If id is used, then return the items matching the id, if not, return everything.

One last thing: in this case, we use data from a mock database, but that data can come from anywhere really, it could be a REST api or even another GraphQL server.

Read the excellent docs and tutorial from Apollo GraphQL to learn more!

Feel free to ask questions in the comments, and clap if you liked this article.

--

--

Arnaud Dostes

Previously Paris and Geneva, currently New York. Can also be found at https://dev.to/arnaud