REST vs GraphQL
POV: a stubborn front-end hardliner
There I was, happily Axios-ing my REST endpoints, when a friend sent me a link to this interesting new toy (his words) that would apparently replace REST. Having invested a ton of time in researching REST’s finer points, and as somebody who has spent more than a few hours–sometimes with beer involved–debating the differences between PUT and PATCH, I thought to myself, “Yeah, right.”
We developers tend to be an opinionated bunch. Proof of that statement is just a Twitter click away, where you might witness an often fruitless debate on a wide variety of things such as browser choice and freedoms related to said choice, whether we should just reinvent the world in Rust, Web3 (scam or legit), and so on. Those are just a few examples from my current Twitter feed. I didn’t even have to scroll that far down, to be honest.
I’m not at all different when it comes to being opinionated and stubborn about a given topic. Trust me, I can get VERY preachy when it comes to classes in JS (and how they were the biggest mistake ever), or about how whoever claimed that React was a good thing when it first came out is either a Facebook employee or a liar. I know, rich, coming from somebody who tends to earn their day-to-day living from React. I did grow to love it, however, and accepted it as the OnE tRuE wAY later on.
Considering the examples above, the fact that I tend to not learn my lessons becomes exceptionally obvious with my first reaction to GraphQL. There I was, happily Axios-ing my REST endpoints, when a friend sent me a link to this interesting new toy (his words) that would apparently replace REST. Having invested a ton of time in researching REST’s finer points, and as somebody who has spent more than a few hours–sometimes with beer involved–debating the differences between PUT and PATCH (and being very opinionated on the topic, once again), I thought to myself, “Yeah, right.”
So first off, I thought the comparison was… inappropriate, to put it politely. Comparing a query language to an architectural style1 would very much be apples-to-oranges. As I later learned, even though they are fundamentally different on a type-of-thing level, they are trying to solve the same problem: They’re a fancy way of calling a function from a remote client2. Said function is often associated with a database query, but that isn’t always the case–it can also be a file, another API, a socket etc. In effect, call something remotely, and maybe get a response, if that is the purpose of the call.
A practical example
The first obvious difference that I encountered from a client-side perspective was how granular you can go with GraphQL when making a query, opposed to the pre-defined sets of endpoints that REST tends to offer. Let’s imagine that we’re trying to fetch a user’s profile. The way one would go about that in REST usually would look something like:
GET https://api.example.com/users/1
{
"id": 1,
"username": "bobTheDev",
"firstName": "Bob",
"lastName": "Smith",
"email": "[email protected]"
}
This is fine if you’d like to get the entire user record and use that locally on your client (or server, see 2), which is often the case. Now, imagine only needing Bob’s email and username for your particular use case. With REST, you’d either have to accept the fact that you’d be dealing with an entire user record and handle that client-side or you could bother the API people to allow you to call a subset. Meaning more work for them, for, objectively, a very small improvement overall. Now, if this was a one time thing, it would have been okay – but you’d probably need a similar thing for every other endpoint which opens more and more work for our API people. Not good. In contrast, with GraphQL, you’d do something like this to fetch said user:
POST https://api.example.com/graphql
with a payload of:// the query part here is optional, up to "{" - we can use it to name the query if we need to:
// https://graphql.org/learn/queries/#operation-namequery {
user({ id: 1 }) {
id
username
firstName
lastName
...
}
}
Note: this GraphQL call and its response is only for illustrative purposes. It will likely be different for different real-world APIs, depending on the toolset used, how the data is modeled and so on.which would return something like:
{
"data": {
"user": {
"id": 1,
"username": "bobTheDev",
"firstName": "Bob",
"lastName": "Smith",
"email": "[email protected]"
...
}
}
}
So far, this is not too different from what we can do with the equivalent REST endpoint. Realistically, it’s even more work for the client-side developer–you get more things to write in that query! However, it’s that exact verbosity that allows you to go as granular or as general as you need to with what you’re fetching. Again, back to the theoretical example where we’d need to fetch Bob’s email and username only; with a small change to the example query above, we can get back exactly that:
query {
user({ id: 1 }) {
username
email
}
}
and get back:
{
"data": {
"user": {
"username": "bobTheDev",
"email": "[email protected]"
}
}
}
It was during a situation similar to this one that it first clicked for me: There’s more flexibility in having a graph to query–opposed to having a set of endpoints, which always give you back fixed shapes of data. Despite the verbosity, the flexibility you get with what you can fetch doesn’t even compare to REST.
In order to fully understand what’s going on with the query expression and the call itself, let’s dissect them a bit and compare piece by piece to their REST counterparts:
The HTTP request and its body
While with REST, you’d use the appropriate HTTP verb to call an endpoint (GET, POST, PUT…), with GraphQL, you’re always using POST. The body of that POST request always contains one or more query
, mutation
, or subscribe
(more on these later). In contrast, in REST, where the body is always the data payload (if it exists and is appropriate for the given verb), the body of Gda beraphQL is always one of the three mentioned above. The payload for a GraphQL call would be in-lined to the body of the query, mutation, or subscribe. Considered from a front-end perspective, one might view REST as being easier here.
Note: I’m repeatedly saying “from a front-end perspective”, because the back-end one is very different—the amount of work that goes into writing a REST vs a GraphQL API is similar and largely dependent on the tooling you’d use. For example, using something like Hasura, Graphile, etc. would provide you with a batteries-included GraphQL API, just from a database schema. If you were to go for something like Apollo instead, you’d lose a lot of that automation, but you’d get all the control with how the server responds to what piece of the graph. The bottom line? YMMV.
The GraphQL syntax
As mentioned above, GraphQL is a query language. As such, it has its own syntax, which would be too big to fully describe in this article. However, we can go over the basics.
Query
Since we’ve been through the basics of a query above, we’ll just expand on how naming works here:
query fetchUser {
user({ id: 1 }) {
id
username
firstName
lastName
...
}
}
The differences to the examples above are minimal, the only difference being that there is a name for the query this time: fetchUser. The payload that we would get back is pretty much the same as in the examples above. So, what then, would be the purpose of using a name? This brings me to the second a-ha moment I had with GraphQL: multiple operations in a single request. Depending on your tooling, it might be good to use it regardless, since debugging and tracking down what happens may be easier with named operations opposed to using anonymous ones.
Let’s get back to multiple operations. Imagine needing an extra piece of data (let’s say we’re making a blog, and we need some posts to show), not necessarily related to user. With REST, you’d ordinarily need to send an extra request. In GraphQL, however, the following is completely valid:
query fetchUser {
user({ id: 1 }) {
id
username
firstName
lastName
...
}
}
// posts is a collection and since GraphQL works, on, well, a graph, you also have edges and nodes
// More at: https://graphql.org/learn/pagination/
query fetchPosts {
posts {
nodes {
id
title
date
author
contents
...
}
}
}
What you’d get back (and again, it may be slightly different depending on the tooling you use across the stack) would look something like:
{
"data": {
"user": {
"id": 1,
"username": "bobTheDev",
"firstName": "Bob",
"lastName": "Smith",
"email": "[email protected]"
...
},
"posts": {
"nodes": [
{
"id": 1,
"title": "My first post",
...
},
...
]
}
}
}
So far, so good. Not only can we get better granularity of a request, we can fetch many different things (as granularly as we need them) in a single request.
Mutation
In GraphQL, a mutation is an operation that mutates data (huge surprise there). Compared to REST, that would vaguely map to what POST, PUT, PATCH and DELETE are supposed to do when run on a route (or a route + identifier). Put another way, in CRUD terms, a mutation covers the C, U and D. This is how a basic mutation would look, if we were, for example, to create a new post:
mutation createNewPost ($title: String!, contents: String!) {
createPost(title: $title, contents: $contents) {
id
title
date
author
contents
...
}
}
To briefly go over some notable additions from what we knew about queries here:
- In addition to naming, we can pass variables (payload) to createNewPost together with their types. The exclamation mark signifies that said field is a required field (non-nullable).
- By calling createPost, we are telling GraphQL to look for a function with that name (AKA a resolver), to pass those variables (the ones preceded by “$”) to said function, and expect a response back for a new post. What you’d get back would be very similar in shape to the responses from a query.
More on mutations: https://graphql.org/learn/queries/#mutations
Note: Same variable rules apply to queries as well, if you need to name a query and pass variables to it. To put it in practical terms, the ({id: 1}) part in the query above wouldn’t usually be hardcoded. Rather, it would be passed as a variable–similarly to what’s happening with the mutation here. More on variables: https://graphql.org/learn/queries/#variables
Subscription
A third kind (and one that still feels kinda wild-west in terms of maturity and implementation to me) is subscription. Implementations and syntax vary across the board, but the operation may look something like:
subscription {
newPost {
id
title
date
author
contents
...
}
}
While not too different from a regular query, the whole point of a subscription is to be a live updating query. In effect, we listen to the changes on the graph on that particular node, and we can react (see what I did there) on those changes client-side.
For a more concrete explanation on subscriptions in a more concrete stack, you could have a look at: https://www.howtographql.com/react-apollo/8-subscriptions/. Fair warning, different tools offer different ways to do this, so don’t take what you read for granted.
Operation Summary
To summarize operations: One can go as granular or as diverse as needed, which is one of GraphQL’s greatest advantages, if you ask me. In order to save something–be it creation, update or deletion–there’s only a single operation to remember, and that’s mutation. While we still get the verbosity of writing queries, it might be worth it for the flexibility we get in return. Subscriptions, as non-standardized as they may seem, offer something that REST doesn’t offer by itself: a way to update data on the client live.
Additionally, GraphQL can come with some more interesting toys (which most of the tools out there are able to do), such as relations (get all posts that belong to a given user from a user query) and nesting queries and mutations (create a new post and fetch it’s author data after creation), just to name a couple. I don’t link any resources on purpose, since these will vary depending on the tool you choose.
Typing, Documentation, and API Discovery
One of my loveliest discoveries was learning that the documentation of what can be called and how wasn’t an optional add-on for GraphQL in most of the tools and libraries. It comes out of the box. There’s no particular magic here–simply the fact that GraphQL uses the types defined and the queries available (schema) to generate a list of what can be called, in what way, and what the expected outcome is of that operation. That, in itself, makes API discovery a no-op. A type in GraphQL would look something like:
type Post {
title: String! // Built-in
date: Date!
contents: String!
author: User! // Also a type of our own
...
}
Types in GraphQL come in two flavors: built-in and user-defined. In the example above, String is built in, and User is defined somewhere in our code. It’s exactly those types that affect our payloads, incoming or outgoing. In order to have a payload in GraphQL, it has to be typed. More on types
Compared to REST, which comes with no expectations of schema, types and documentation, this was a breath of fresh air. We’ve all dealt with undocumented, semi-documented, or downright wrong documentation for APIs. You don’t really get that choice with GraphQL. As an added bonus, if you’re using TypeScript, there’s plenty of tooling out there that allows you to sync the types between a GraphQL API and a TypeScript codebase that would consume said API.
All of these are largely a question of tooling, but in my experience as someone who happens to spend most of his working hours on the front-end, that tooling has been significantly more advanced for GraphQL.
A final point on API discovery: Proper REST can have discovery built in. Most, however, don’t. See here for a more in-depth treatise on hypermedia APIs: https://kostasbariotis.com/making-a-rest-api-discoverable/.
Using REST with GraphQL
As I briefly mentioned above, a common use case is GraphQL as a “front-proxy”. I recently worked on such a project, in which we used a GraphQL API as our only access point to the server on the front-end. On the back-end, the GraphQL gateway server didn’t communicate to any databases directly, but instead to a bunch of microservices, all of which exposed a REST API. In turn, the GraphQL server had resolver functions that knew which endpoint to call on what query.
The advantage here is, once again, flexibility–you don’t have to go all in. Due to the nature of the idea, a “graph of resolvers”, you can pretty much call anything that makes sense to call. Databases, files, APIs–it’s all fair game.
Where I Wouldn’t Use GraphQL
I wouldn’t use GraphQL when implementing a protocol. I once had the opportunity of implementing TUS. TUS, which comes with an exceptionally well-defined spec, is widely used and is, in my opinion, an absolute pleasure to work with. Doing it via GraphQL would be significantly harder due to the overhead involving translating the TUS endpoints to queries and mutations. I’m sure it can be done, but it feels impractical. Even in a scenario in which we’d have a functioning GraphQL gateway in front of a TUS API, I’d rather use the HTTP calls directly, because that would be simpler to solve a concrete problem opposed to “talking” to a graph.
The Other Side of the Story
By now, I probably sound like:
And while there’s truth to that meme, I’d like to offer a more in-depth view of the similarities, differences, advantages and disadvantages of REST and GraphQL from the perspective of someone who builds those APIs: https://goodapi.co/blog/rest-vs-graphql. The author makes some very good points on the previously mentioned complexities of REST and the category of so-called REST-like (or HTTP) APIs.
Summary
Once I saw the error in my ways, I learned to like GraphQL. In my experience, it was a real boon for productivity. From a front-end perspective, the mental overhead for planning how and when to call data is simply out of the equation. API documentation is just there, without any surprises. As a result, you bother the back-end people less (because hey, they’re busy too, you know!), and you already know what you can call, what to expect from those calls, and the types of things you get back. Your software becomes more stable and predictable because of the non-speculative nature of GraphQL. Your end users also win, because the application is faster by virtue of having to fetch less data and more concrete payloads.
As a final takeaway, opinions are fine. Just try not to allow them to turn you into a grumpy hardliner (as yours truly tends to be).
- Ages ago, REST used to be called a protocol, which feels incorrect in many ways. I’d like to think about REST as described in this SO post (namely, a design style for protocols).
- A server can also be a client to another server, which I expand on a bit later–not just client in the often-used sense of a client-side application, such as a mobile or web application.
Darko Bozhinovski is one of the many talented developers available to hire on the Gun.io platform. Check out his profile, or head over to his personal website, darko.io.
Interested in working with Darko, or one of our other amazing devs on Gun.io? We specialize in helping engineers hire (and get hired by) the best minds in software development.