REST, or Representational State Transfer, is a design pattern for mutating and querying the state of resources between client and server. Resources stored in a database are queried for and mutated by a client using HTTP requests to transport the “current state” of said resource across the wire. 10 years ago, this was a profound idea. REST’s predecessor SOAP, or Simple Object Access Protocol, was a complete and utter horror show to work with, requiring verbose multi-handshake requests to be sent back and forth to negotiate state changes that were difficult to implement and incredibly slow to parse. The RESTful pattern changed the game with a simplistic DSL based on HTTP verbs and consistent data shape across the wire.
For all of the good RESTful APIs created for the developer community, their benefits usually do not outweigh their costs when it comes to building single page web applications or distributed systems, at least if you are optimizing for code simplicity and performance on the wire. We’ve all seen the tragic case of a client making 5–15 HTTP requests to fetch all of the required data to load a new view. Even with a declarative solution like a client-side ORM, the performance penalty is staggering.
Let’s look at an example. One day, your product manager walks into your office and tasks you with creating a new “user profile” view for your single page SaaS application. On a user’s profile, we want to display all of their personal information along with their billing history, last 10 interactions with other users in some sort of timeline widget, and their top 5 friends. Because the profile is editable, we also need to make sure that we load the latest state of the user onto the client so that we don’t edit and persist stale data. So, at our root, we have a User resource that contains edges to Transaction resources, User resources as friends, and Interaction resources. In the purely RESTful world, where every resource has a strict type (or shape), this would require that at least 5 different RESTful routes are provided to the client along with 5 different HTTP requests against said routes.
GET /users/:idPUT /users/:idGET /users/:id/billing_transactions
GET /billing_transactions?user_id=:idGET /users/:id/interactions?num=10
GET /interactions?user_id=:id&num=10GET /users/:id/friends?num=5
Right off the bat, we notice that in order to render the page we must make a whopping 4 HTTP requests and bear the performance cost of doing so by way of a less responsive experience. You may also notice that in some cases we can add URI params to add branch logic to our routes. This may seem convenient, but there is definitely a cost in terms of surface area and cyclomatic complexity for each route controller… meaning more potential for bugs. A less “pure” approach to make fewer requests could be to do something like this:
GET /users/:id?include_transactions=true&include_friends=true&num_friends=5&include_interactions=true&num_interactions=10POST /users/:id
If you can’t immediately see a problem with the above, take a second and think about it. While it might save on performance, we now likely have a mega-controller with never-ended branch logic creep every time we decide that we want to fetch yet another edge of the user resource. Six months from now, I pity the person who has to maintain that.
Now let’s make it even more interesting with a new “challenging but not unreasonable” product requirement. Our product manager decides that in order for the user to truly enjoy the experience of using their shiny new profile, the user should also be able add and remove tags (a new Tag resource) to their user account. These tags are arbitrary, shared between all other users, and must be constrained unique. Typing into the “tag box” will search the server for tags matching the same string and “suggest” that the user selects one of the existing tags. If the user presses enter with a string that doesn’t currently exist, it should create the tag when the user saves his profile. Likewise, if a user adds or removes an existing tag, it should add/remove a relation between the user and the existing tag resource. All changes to the User’s profile AND their Tags should be persisted when the save button is clicked. We need two new endpoints:
For associations, we will simply pass a mutable array of tag ids on the User resource between client and server. A save operation now looks something like this:
POST /tagsfor each new tag
POST /user/:idpassing the new user data along with an updated array of Tag ids attached to the user
If we care about our work, we immediately see an edge case that makes us feel uneasy: What happens if we create tags successfully but then fail to update the User?
In this case, we should really be using a database transaction to ensure that tag creation and user updating is a single atomic interaction with our data store. We don’t want users “leaking” tags into our backend every time they pass invalid characters for their new tagline. What’s more, we don’t want to write reconciliation code on the client to ensure that subsequent saves after the user removes the invalid characters use the tags that we already created back when our user update operation failed. We want the mutation of state related to the view and the intended interaction to be treated as an atomic unit, because this is what the end user and developer would expect.
Usually this is the point that RESTful APIs fall apart and ad-hoc single purpose endpoints for
PUT are created to persist values atomically. But now we have a maintenance nightmare. Every time we change our view, we have to remember to also change our ad-hoc endpoints. We would also need to document the hell out of these one-off endpoints, because developers expect RESTful APIs to be, well, RESTful… which this is not.
Over the past couple of years, several new solutions have emerged that attempt to solve the performance and complexity problems espoused by RESTful APIs. The top three are Netflix’s Falcor, Google’s gRPC, and Facebook’s GraphQL. Each has it’s own distinct set of benefits:
- Falcor is a loosely typed transport that allows you to think of your entire backend as a single JSON object that you traverse with path strings to query and mutate multiple sub-sections in a single request over HTTP1.1 on all major platforms.
- GraphQL is a strongly typed transport that allows you to think of your entire backend as a graph that you traverse with a strongly typed DSL to query and mutate transactionally over HTTP1.1 on all major platforms
- gRPC is a strongly typed transport that allows you to make function calls between client and server over HTTP2 on all major platforms except web (currently).
All three options are potentially beneficial based on your own use case. However, in this post, I will be sticking with GraphQL alone which, I feel, provides a nice mix of platform support on an already hardened HTTP1.1 along strongly typed interactions between client and server.
What are the benefits of GraphQL over RESTful APIs?
To be continued…