I've been hearing about GraphQL lately, a library to create an API endpoint for your data. GraphQL has been positioned as a replacement for our traditional REST backends as it tries to solve the same problem. Which is, to make data available for our web or mobile apps, but with a different approach that is supposed to be more flexible.
To be honest, I was quite skeptical at the beginning. What's wrong with our beloved REST endpoints? And also, do we really need another "type" system on top of the type system of our favorite Object Realtional Mapper (ORM)/Object Document Mapper (ODM)? What if, I also like to work with Typescript (which I do)? Don't get me wrong, I like a good old type system for my code, but maybe not that much.
Note: You won't see many static types in the snippets shown below but Typescript is more than just about types, it lets you use advanced JavaScript features like import.
Migrating JSON Placeholder
Because I was curious and wanted to see what all the fuss was about, I decided to migrate the data from jsonplaceholder, an open and very popular REST API, to a GraphQL service. This migration will provide an opportunity to compare head to head a well structured REST API to GraphQL for fetching data only (GET actions in REST). Due to time constrains on my part I won't be covering "mutations" (POST, PUT or DELETE actions in REST).
The first thing to notice is that JSON Placeholder provides data for six different entities: users, posts, comments, albums, photos and todos. Each one of those resources has its own id field which makes me believe that the underlying database is relational.
Just for fun, and because it seems to be the usual choice when working with node, I decided to use mongodb as my database engine. Given the structure of the data I decided to create three main documents:
- users with todos as embedded subdocuments.
- posts with comments as embedded subdocuments.
- albums with photos as embedded subdocuments.
The structure of the database can be understood by looking at the mongoose schema:
const todoSchema = new Schema({
title: String,
completed: Boolean
});
const userSchema = new Schema({
_id: Number,
name: String,
username: String,
/* ...other fields... */
todos: [todoSchema],
posts: [{ type: Number, ref: 'Post' }],
albums: [{ type: Number, ref: 'Album' }]
});
The other two documents are created in a similar way. You can see the details in the models folder of the repo.
Defining the GraphQL Schema
Now that we have the database schema, it's time to define the GraphQL schema using the "GraphQL Schema Language" (GSL):
type Todo {
title: String
completed: Boolean
}
type User {
_id: Int!
name: String
username: String
/* ...other fields... */
todos: [Todo]
posts: [Post]
albums: [Album]
}
Does it look familiar? The GraphQL schema is very similar to the database schema created above with mongoose with two important differences:
- GraphQL schemas are created using GSL which is not JavaScript. Notice that in JavaScript there isn't a type called Int and that in GSL schemas fields are not separated by commas.
- In the mongoose schema we had to define extra information for the database, like which subdocuments are embedded and which are just linked. This database implementation details are transparent in GraphQL.
If the schemas are so similar, why do we need both? The answer is that GraphQL is a library that can be used with multiple languages (js, ruby, python, java, etc.), multiple databases (mongodb, postgresql, mysql, etc.) and multiple ORMs/ODMs (mongoose, sequelize, etc.). To achieve this interoperability, GraphQL needed to have its own way to define the schema that is generic enough to satisfy the needs of our consumer applications and the backend technology used. Thus GSL, the GraphQL Schema Language.
Connecting GraphQL to Koa
To make the GraphQL service available to our consumer applications, we need to integrate our GraphQL schema with a webserver, in this case Koa. To do this integration, we are going to need to install a couple of extra npm packages besides graphql and koa.
Note: The reason to use Koa instead of Express is that, as the their website states, "Koa is a new web framework designed by the team behind Express". In short, Koa is just the evolution of Express.
$ yarn add graphql koa koa-mount koa-convert koa-graphql
With these packages and our GraphQL schema, we can now write our webserver.
src/server.ts
import { buildSchema } from 'graphql';
import * as Koa from 'koa';
import * as mount from 'koa-mount';
import * as convert from 'koa-convert';
import * as graphqlHTTP from 'koa-graphql';
import './connect';
import { User } from './models';
const HTTP_PORT = 3000;
const app = new Koa();
const mySchema = buildSchema(`
type User {
name: String
}
type Query {
users: [User]
}
`);
const root = {
users: () => User.find().populate('posts albums')
};
app.use(mount('/graphql', convert(graphqlHTTP({
schema: mySchema,
rootValue: root,
graphiql: true
}))));
app.listen(HTTP_PORT, () => console.log(`Running in port ${HTTP_PORT}`));
There's a lot going on here so let's review again the important pieces beginning with the schema.
const mySchema = buildSchema(`
type User {
name: String
}
type Query {
users: [User]
}
`);
First, I'm not defining all the fields of the type User, I'm only defining the name field, just to verify that it's actually working before continuing to add more code. To see the full schema definition look at the server.ts file in the repo.
Second, we need to define the type Query, as it is mandatory and it is where we define our real "endpoints" in GraphQL. Just like in JSON Placeholder, we had different urls for users, posts and albums which gives us back the list of each respective resource. The same can be done in GraphQL using the type Query. In this particular example, we are stating that there is a query (or "endpoint") called users that, when called, will give us back an array of Users.
How does GraphQL know how to get the list of users from the database? That's what resolvers are for:
const root = {
users: () => User.find().populate('posts albums')
};
User is a mongoose model that exposes handy methods like find(), which returns all the documents in the collection. The populate('posts albums') is just a way to indicate mongoose to perform "joins" with linked collections. In this case, to add all the user's posts and albums that are stored in the Posts and Albums collections.
Notice that both, the Query type and the root resolver, have a property called users. Every time we ask for the users query, the associated resolver function is invoked and the list of users is fetched from the database and displayed to the client. The GraphQL query to get all the users will look like this:
{
users {
name
}
}
Note: The language used in the above snippet is called the "GraphQL Query Language" (GQL). Notice that it is not the same as the GraphQL Schema Language (GSL) used before.
Mimicking JSON Placeholder API Endpoints
At this point, we know how to emulate the /users endpoint, but how can we emulate the individual user search by id like /user/1? For that, we need to create a new Query property.
const mySchema = buildSchema(`
type User {
name: String
}
type Query {
users: [User]
user(_id: Int!): User
}
`);
This new query called user returns an individual User instead of a collection. Another important difference is that this query expects an argument, the _id of the user (keep in mind that we have renamed the id from JSON Placeholder to _id to follow mongodb structure). Finally we are using the ! operator to define that the argument is required.
Again, every time we create a new query, we need to define the associated resolver:
const root = {
user: (args) => User.findOne(args).populate('posts albums'),
/* ...other resolvers... */
};
The user resolver is going to receive an object with an _id key in it so that we can use it to find the individual user in the database. From the client code, we can now request a particular user like this:
{
user(_id: 1) {
name
}
}
As a side note, instead of creating a new query in our schema definition, we could have modified our existing users query and resolver like this:
const mySchema = buildSchema(`
type User {
name: String
}
type Query {
users(_id: Int): [User]
}
`);
const root = {
users: (args) => User.find(args).populate('posts albums'),
};
With this schema and resolver, the following queries in your client code would have been valid:
Get all the users
{
users {
name
}
}
Get user with id 1
{
users(_id: 1) {
name
}
}
The drawback is that in the latter, we would have gotten an array with just one element and our client code would need to unpack the value. That's why I think that creating a separate query is the best choice.
Beyond Simple Queries
Creating more complex queries in GraphQL is easy. What if we want to query a user by username or email?
const mySchema = buildSchema(`
...
type Query {
users(_id: Int, username: String, email: String): [User]
}
`);
const root = {
users: (args) => User.find(args).populate('posts albums'),
};
Nothing changes in the resolver and the client app doesn't break. Now we can find a user like:
{
users(email: 'Sincere@april.biz') {
name
}
}
What if we want to find a collection of user by name? Easy:
const mySchema = buildSchema(`
...
type Query {
users(name: String): [User]
}
`);
const root = {
users: ({name}) => {
const regex = new RegExp(name, 'i');
return User.find({ name: regex }).populate('posts albums');
}
};
Now we can find all the users whose name contains the string 'Pat' like:
{
users(name: 'Pat') {
name
}
}
Conclusion
GraphQL is definitely a powerful library to easily create flexible data endpoints. Providing the client application with a mechanism to query data from the backend means optimizing the usage of the internet connection, which for mobile apps is usually a big win.
Although it's possible to define the GraphQL schema using pure JavaScript instead of GSL, I still prefer the latter giving how concise the schema definition is, and how intuitive it is to use the typing system. It reminds me a lot of Typescript.
In my opinion, there's two drawbacks of using GraphQL for the backend. First, the repetition of the schema definition between mongoose and GraphQL is tedious to write at the beginning and harder to maintain in the future. Luckily, there are tools out there like mongoose-schema-to-graphql that allows us to create the GraphQL schema automatically from the mongoose schema. My other concern is that by adding yet another level of complexity in my code, it could make my overall backend application more fragile because more things can go wrong. However, in the end, I guess that's just another day of a JavaScript developer.
You can find the source code here. Install the app and play with the graphiql interface to see the real power of GraphQL.