The Pros and Cons of GraphQL
A little over a month ago, Facebook marked GraphQL—open-sourced for more than a year—as production-ready ( http://graphql.org/blog/production-ready/ ). Almost simultaneously, GitHub opened early access to its GraphQL API ( https://developer.github.com/early-access/graphql/ ). Two major announcements landed in quick succession. Does this mean GraphQL—now more than five years old, born almost at the same time as Facebook’s News Feed—will shine in the days ahead and gradually displace REST APIs?
REST API
Let’s start with a simple example to illustrate the similarities and differences between REST and GraphQL. Suppose TubiTV has an API and wants to fetch a user from the system; for a REST API, the endpoint would look roughly like this:
GET /v1/users/1
The response would be:
{
"first": "Tyr",
"last": "Chen",
"email": "...",
"bookmarks": [ "12345", "12346", "12347" ],
"total_view_time": 10800
}
Here, bookmarks are the movies the user has favorited. If the client calls this API to display the user’s profile, this API alone is not enough, because bookmarks is not expanded and cannot be rendered directly, so we would probably expose another endpoint:
GET /v1/users/1/bookmarks
The response would be:
[
{
"id": "12345",
"title": "Doctor Strange",
"description": "...",
"poster": "//cdn.tubitv.com/12345.jpg",
"url": "//cdn.tubitv.com/12345-1280x714-,559,1414,2236,2642,3628,k.mp4.m3u8"
},
...
]
Such APIs are clean, each doing its own job, but they are not client-friendly. To get all the data needed for rendering, multiple requests are required. Especially for mobile clients, any redundant request badly degrades the user experience. Therefore, APIs like this often compromise:
GET /v1/users/1/?expand=bookmarks
By supplying an extra parameter, the API returns content containing bookmark details. This compromise morphs with every kind of requirement—for example, allowing the API to return partial fields:
GET /v1/users/1/?fields=first,last,email,bookmarks(title,poster)&expand=bookmarks
These assorted patches each fight their own battle, lack a holistic view, and easily affect one another, much like Ptolemy’s geocentric model. At first everything was fine; as astronomical observations advanced, the model had to add more epicycles to accommodate observed data, until the theory buckled under its own weight and was replaced by heliocentrism.
In the example above, if the caller forgets to include bookmarks in fields, then expand does useless work; conversely, if fields includes bookmarks(title,poster) but there is no expand, an erroneous result is returned. REST APIs that compromise between the ideal and reality make other things difficult too. The user API we use here, merely supporting both fields and expand, makes the swagger doc (or other docs such as RAML or json-schema) incredibly complex to write. What? You never write swagger-like docs? Then how do clients explore the API’s capabilities?
API versioning and retiring old versions need no discussion—it is all tears—the world is full of iOS/Android users who resist progress and would rather die than upgrade.
GraphQL API
REST’s many problems in the real world gave birth to GraphQL. As an API query language, GraphQL starts from the product’s perspective and aims to make APIs flexible enough to handle complex, ever-changing user scenarios. The user API above can be accessed like this:
{
user(id: 1) {
first
last
email
bookmarks {
id
title
description
poster
url
}
total_view_time
}
}
If some part of the product does not need to access bookmarks, query it like this:
{
user(id: 1) {
first
last
email
total_view_time
}
}
To achieve this, GraphQL defines a complete type system. The server declares the types of its data to tell clients what it can do, so the schema is also a contract. The REST API ecosystem itself and its various frameworks never defined a suitable type system; this spawned many scattered, incomplete implementations, or implementations that lean on tools like swagger. GraphQL also defines a rigorous query language. REST APIs have achieved nothing here: query strings and request bodies mostly follow no rules, and everyone improvises. As a result, GraphQL can easily generate powerful validation tools from the type system and user-defined schema, ensuring that a query is correct and satisfies the server’s capabilities.
Let’s look at sample server code that handles the query above:
const UserType = new GraphQLObjectType({
name: 'user',
description: 'User profile',
fields: () => ({
first: { type: GraphQLString }
last: { type: GraphQLString }
email: { type: GraphQLString }
username: { type: GraphQLString }
bookmarks: {
type: GraphQLList,
resolve: user => user.getBookmarks()
}
total_view_time: {
type: GraphQLFloat,
resolve: user => fetch(URL + user.id).then(res => res.json())
}
})
});
const QueryType = new GraphQLObjectType({
name: 'Query',
description: 'The root of all... queries',
fields: () => ({
user: {
type: UserType,
args: {
id: {type: new GraphQLNonNull(GraphQLID)},
},
resolve: (root, args) => UserModel.findById(args.id),
},
}),
});
export default new GraphQLSchema({
query: QueryType,
});
The code above is easy to understand, so I will not elaborate. One thing worth noting: the resolve function of a field returns a Promise, so you can do many interesting things. For example, total_view_time here comes from an internal service. From this angle, GraphQL handles heterogeneous data sources cleanly and concisely. Traditional REST APIs are not incapable of handling heterogeneous data sources; it is just that such code is less readable. From this perspective, GraphQL is well suited to serve as a thin API gateway, a bridge between clients and various internal systems (including REST APIs).
Earlier I said GraphQL clients can flexibly compose various queries within the server’s capabilities. This capability is good for backward compatibility and version control. REST APIs often evolve with changes to the API version, whereas GraphQL APIs largely do not need this. For the same API, the server only needs to add a new field; new clients use the new field in their queries, without affecting old clients. This is a very elegant evolution scheme.
Caveats
I have discussed many of GraphQL’s virtues; next I will discuss caveats when using GraphQL for APIs.
GraphQL Does Not Automatically Make APIs More Efficient
GraphQL defines only the interface layer of an API; whether it is more efficient than REST depends on the implementation. In fact, an unoptimized GraphQL API will generally have lower performance than a REST API. Because GraphQL resolves each field separately, N+1 queries appear easily. Node.js’s GraphQL implementation takes this into account and provides loader to allow more efficient querying. Therefore, when resolving, you must use loaders appropriately.
In addition, because GraphQL and Relay are often mentioned together, some people attach Relay’s capabilities to GraphQL and think that partial-data loading is GraphQL’s doing; this is not the case. Using Relay adds complexity to both the client and the server. If you are just starting to use GraphQL, I do not recommend introducing Relay directly. You have to eat rice one bite at a time.
GraphQL’s Flexibility Is a Double-Edged Sword
Life is lived with one problem stacked on another; software architecture is organized with one trade-off stacked on another. GraphQL’s powerful, flexible querying makes one’s mouth water, but it hides many security and performance problems. Suppose user has friends; a client can query:
{
user(id: 1) {
first
last
email
friends {
first
friends {
first {
friends {
...
}
}
}
}
}
}
Such a query would be impossible in a REST API, but in GraphQL it is a legitimate query, and without your writing any code, the query executor will faithfully run your resolve functions over and over until it exhausts system memory or overflows the stack. Therefore, although in theory queries can nest without bound, a real deployment must specify a nesting limit and define a timeout for each API, killing the query once it exceeds the timeout.
Existing Caching Mechanisms May Stop Working
In the REST API world, we can use nginx cache or HAProxy to cache APIs at the load balancer level—if the API is idempotent, then the same input yields the same output, so it can be cached. With GraphQL, because queries are generally large, they are typically submitted via POST; POST is not idempotent, so the existing caching mechanisms stop working. If your API’s performance depends heavily on load-balancer-level caching, pay special attention.
GraphQL May Increase Implementation Complexity
For programmers, using GraphQL means they must learn something new. Some originally simple CRUD APIs become complex under GraphQL.
Conclusion
GraphQL is now a mature technology, and almost every language has some support for it. If you are going to adopt GraphQL, be sure to control its flexibility and do proper performance benchmarks. If you want to migrate an existing API system to GraphQL, you can initially use GraphQL to wrap existing REST APIs, letting client engineers experiment freely. Then, according to importance and urgency, gradually rewrite existing APIs one by one; never start by tearing everything down and rewriting from scratch.