My Argument for GraphQL

Today I want to express my reasonings for getting on board the GraphQL bandwagon. This is nothing new, I've been a fan for at least the last two years, I even tried selling it in an interview. Well, now I'm putting my thoughts out there so I have a resource I can refer friends and coworkers to when I'm trying to make a big pitch. But first let's start at the beginning.

Graphs

I don't think anyone really talks about this aspect of GraphQL all that much, maybe it's not that important; but I think it's an important place to start.

Early on in my career I was assigned to put together an experimental project: put a web-server into a graph database.

It sounds crazy, because it is crazy. It is a unique idea though, I thought it was cool.

This was the first time I really got a chance to work with graphs. I already had written quite a few personal projects and a lot of class assignments, and of course working through tasks at this new job. This project was given directly to me, so I had a chance to prove my chops at running a project and building something from scratch. I was also totally green to the concept of graphs and graph databases, and I think that actually worked out in my favor. Graphs have a way to "just make sense," at least from a high-level on paper, they are easy to understand and navigate. The underlying bits of optimizing graph operations is not so simple.

For the uninitiated, a graph consists of "nodes" which contain some data and edges which link two nodes. There is a distinction here where a graph can be "directed" or "undirected" which refers to the whether or not the edges "point to" a node. Some graphs might just represent relationships with an edge, no specified direction while others might apply meaning to the edge in the form of a direction. A "next" edge might point from a node to another node implying not only that there is a relationship but that it is probably some kind of "ordered" relationship.

// undirected
(5)--[NEIGHBOR]--(6)--[NEIGHBOR]--(7)

// directed
(5)--[NEXT]-->(6)--[NEXT]-->(7)

At the highest level, building a website with a graph was going to require some creativity. Obviously, the main bit is going to be using nodes and edges (I may begin using the term relationships, conceptually they're the same thing) to represent the structure of the website, how each piece "connects," so to speak. So instead of basic typed nodes each node would have to serve a greater purpose like starting with a "Route" node, and then having a "Controller" node which could link to a "View" node which might import some "Style" or "Library" (front-end code). Getting the nodes linked up like this made the structure of the website kind of click.

(/index)--[CONTROLLER]-->(<some backend code>)--[RENDERS]-->(<HTML>)

(maybe one day I'll draw real graph pictures)

When the proof of concept was shown off the other team members were pretty blown away by how much sense it made. Need jQuery on this page? Link the jQuery library node. Need some code behind it? Link some code nodes, build your router with route nodes, data was stored alongside the website structure as nodes as well. Overall I think it was a huge success at proving it's point. And when viewing the nodes from the top down in the graph you could easily see how communication was happening and how views were being rendered.

I take this tangent because I feel it's necessary in establishing how a simple graph can take a complex problem like a web-server and make it structurally easy to comprehend and think about. This can then be applied to other problem spaces (although it is not a one size fits all, you should ensure a graph really makes sense before employing it). This basic understanding of how to use a graph for storage and access and the kinds of operations you can do with graphs was eye opening. As I become more versed with Neo4j, the graph database we were using, I realized that moving from their default REST API interface to their Cypher interface (basically SQL for graph databases) could cut my query times by more than half allowing me to request all the necessary nodes in fewer queries (in most cases, only 1!). With the REST API I had been forced to manually fetch each node one-by-one before I could start operating on their relationships and determining the final output of the request. It was liberating being able to ask for exactly what I needed from the route node all the way to all the bits and pieces to process in a single go.

When another team member joined the project he started building a UI. Before this point I had been working directly out of the web interface the graph database provided. Obviously with a UI he required endpoints to fetch node data and it was impressive how easy it was to reuse a lot of the code around the queries and respond with only what was needed. When the UI loaded it fetched the root route node, which would show it's type and some data, and a few bits of information about its edges. When clicked it would expand and fetch basic information (like type) from the nodes connected to it which could be previewed and we could later perform full requests for full node data. This all happened with very few actual endpoints, and the queries were largely the same, the only major difference was what was being fetched.

Finally the point of all this:

This mirrors the main aspect of GraphQL that appeals to me. It is your API. REST is awesome, I'm not here to convince you otherwise, and I'm not a GraphQL purist. It doesn't make sense for every API call to go through GraphQL, like file uploads or data that is very dynamic (like storing a user-controlled map of key/value pairs). If it makes sense for your project to have REST endpoints and a GraphQL endpoint, more power to you. GraphQL is not the solution to every problem (after all not all problems are nails, sometimes you have to get a screwdriver).

With GraphQL you don't really design the API so much as you just kind of describe what the data looks like and how you can manipulate it.Then the query language (the QL part of GraphQL) lets your users do what they want with the data. Your profile menu can fetch just the users display name and avatar URL, a user show page can fetch more information about a user. This is not two different endpoints, it's just two queries against a single dataset.

What I Think GraphQL Solves

Let's take a look at how you might develop a REST endpoint, and compare to how this would evolve with GraphQL. I'm aware this is anecdotal and in favor of my pitch for GraphQL but it's also based on my experience.

So let's assume we have a blogging site, they are pretty simple to understand the the data layout is what you probably expect. A user can own a blog, with posts, posts can have comments, users make said comments, etc...

We would most like start with some core routes, GET/PATCH /posts/:id and POST /posts and that will work out pretty well.

To show a post to the user we would rely on GET /posts/:id and we would need a lot of information about the post with some information about the user who created it, and comments related to the post. We would probably manifest this with the response below, containing information about the post and it's author (a user) as well as it's comments which also are owned by a user (author) and we get username data for each user we encounter.

{
  "title": "This is a title",
  "body": "This is the content for the blog post.",
  "author": {
    "username": "imauser"
  },
  "comments": [
    {
      "body": "This is the body of a comment",
      "author": {
        "username": "imnotthesameuser"
      }
    }
  ]
}

This is a great start. This gives us what we need to start rendering posts to users. Now we need a page that lists all the new posts, the "index" page. We don't want to have all the post information on this page, we just want some little blurbs and maybe include information about the author. So, assuming we have a "summary" field that contains a summarization of the post body we could structure our GET /posts response like the following:

[
  {
    "title": "Blog Title",
    "summary": "Blog sumar...",
    "author": {
      "username": "imathirduser"
    }
  }
]

Now we may be starting to see two issues with our REST API. First off, one request returns an object and the other request returns an array. Maybe a quick solution is to make them all return objects like {"post": ...} and {"posts": ...} and we can get what we need off our root key. Solved that. Next we see we have the author object repeated. We use that in a few different places. So we set up the back-end so that all three use the same JSON renderer for the author data. Seems solved.

The next big feature we add is private posts. This requires users to be authenticated to read a post (not that private). So now when a request comes through we might need to display some error messages. Our first attempt, just testing our logic works, has us return something like:

"You must be logged in to view this post."

If you recall from about the remark about the endpoints returning two different kinds of responses (object vs array) and we made them both return an object, well we should stick with that pattern we're developing and provide the error as an object.

{
  "post": {
    "error": "You must be logged in to view this post."
  }
}

But that feels ugly. Not only do we have to know that this request returns an object with a "post" key but we also have to know to look for an "error" key before assuming the request was successful. Sure error codes could give us this insight. Let's try removing the false positive "post" key and just have a general error response.

{
  "error": "You must be logged in to view this post."
}

All right, now we know if we have an error key, we can render our error feature and if we don't have an error key we have a successful response and can move on down the happy path.

Next we want to add profile pages, where you can view information about a user. We already have our basic user renderer that renders the username, and we know we don't really want to add a bunch of data for it so we add a second user renderer (similar to how we have two post renderers, one for show and one for index) to render more data about a user for this profile page; but we also want to reuse our index post render but just for the user being viewed. So we take the effort to make the index post renderer a bit more generic and usable from any context and we have our profile page data.

Right now, Our API is starting to grow and we have no real hard decisions made on structure. Since we're about to expose this API to the public we decide to pick a clear standard we can point to when writing responses for not only ourselves and other developers that might be building our blog project, but for users consuming our API. We might select something like JSON:API. That changes our responses to something like:

{
  "data": {
    "type": "post",
    "id": "100",
    "attributes": {
      "title": "...",
      "body": "...",
      "summary": "...",
      "author": {
        "id": "10",
        "type": "user"
      }
    }
  },
  "included": [
    {
      "data": {
        "type": "user",
        "id": "10",
        "attributes": {
          "username": "imauser",
          "posts": [
            {
              "type": "post",
              "id": "100"
            },
            {
              "type": "post",
              "id": "110"
            }
          ]
        }
      }
    }
  ]
}

Whew, that's a big one. But we're standardized. We have some pretty basic response variations, filtering user information down for post requests and filtering post requests down for index and user profile routes. Our new API consumers are also probably not going to use the API exactly like we are so they are bound to encounter some endpoints returning either too much or not enough with no middle-ground endpoint that is just right. To solve this problem we might look to taking query parameters to let users dictate what fields we include for which types, and which related objects we also include and which fields to include for those objects...

We are live though, so changing API behavior means we should version it, to prevent breaking changes. Now we are managing multiple versions of endpoints, some taking some fancy query parameters that allow the user to dictate the response they want, but still kind of sticking the REST resource pattern so it's not possible to build every users perfect request.

Time For GraphQL to Step Up to the Plate

So now let's rewind time. We're building a bloging platform, and we've chosen GraphQL. Right off the bat this works out great because we are given:

A) A general format for requests (the query language)
B) A general format for responses (GraphQL libraries will return a "data" keyed with our requested fields or an "errors" field with errors)

Next, we need to enable the post show page. At this stage we have three resources and we'll throw them into our GraphQL schema. This schema is us defining what types our API contains and what fields each type has. The "type Query" and "type Mutation" parts are special GraphQL types that represent things we can do at the top level of the query.

type User {
  id: ID!
  username: String!
  posts: [Post]
  comments: [Comment]
}

type Post {
  id: ID!
  title: String!
  body: String!
  summary: String!
  author: User!
  comments: [Comment]
}

type Comment {
  id: ID!
  body: String!
  author: String!
}

type Query {
  post(id: ID!): Post
  posts: [Post]
  user(id: ID!): User
}

type Mutation {
  createPost(title: String!, body: String!, author: ID!): Post
}

schema {
  query: Query
  mutation: Mutation
}

Just by defining this schema we've exceeded feature parity with the REST API we ended with. This assumes we've already written solutions to all the add-on problems like versioning and options field sets.

A simple query like this will fetch a post by ID (10) and return it's title.

query getPost {
  post(id: 10) {
    title
  }
}

Would return:

{
  "data": {
    "post": {
      "title": "I am a Title"
    }
  }
}

The resolver for the post(id: ID!) field would essentially look like SELECT * FROM posts WHERE id = ? and you'd substitute the value given for the id: argument. From there, and most frameworks handle this automatically, resolving the title field on post would just look up that key in the data for post. The specifics of which heavily depend on language and framework in use. These kinds of basic data resolvers are so common they're usually the zero-value for a resolution function.

So what did this buy us? Well, we now have all the endpoints we need for our personal use and public use. Need a post for the show page?

query getPost {
  post(id: 10) {
    title
    body
    author {
      username
    }
    comments {
      body
      author {
        username
      }
    }
  }
}

You can even get fancy, we do author { username } in more than one place:

fragment authorName {
  author {
    username
  }
}

query getPost { 
  title
  body
  ...authorName
  comments {
    body
    ...authorName
  }
}

Need all the posts for the show page:

query getAllPosts {
  posts {
    title
    summary
    author {
      username
    }
  }
}

And so on.

Defining the objects, and corresponding resolution functions, enables us to handle any type of request we might need on those objects. We're also rather seamlessly turning our basic SQL database into a fancy graph database. How so? Well look (with our current schema):

query fancyGraphs {
  post(id: 10) {
    author {
      posts {
        comments {
          author {
            posts
          }
        }
      }
    }
  }
}

This is essentially (from inside out): "Give me every post, for every comment author on all the posts for the user that wrote this post." This is most definitely a nonperformant query without some intense optimizations and probably not useful at all. This example shows the expressiveness gained from GraphQL within the confines of it's query language we were able to easily build this graph-esque request on top of an ordinary, non-graph data store.

Just like unlocking the full power of optimizing the response for the "website in a graph database" project, as well as reusing graph queries for the UI endpoints. GraphQL turns a mundane RDBMS into a powerhouse graph database where your queries are only restricted by your imagination (within the computational capabilities of your back-end).

In Conclusion

Sorry if you don't fully grasp GraphQL from this, it wasn't intended to be a 101 on GraphQL. This was simply written to express the power and capabilities of using GraphQL over alternatives like basic REST API structures. So I hope you have learned something about why GraphQL is beneficial and how it works.

The GraphQL schema not only defined what data types we have, and their fields (documentation!), but the query type we created the endpoints of our API. The schema also has the benefit of become relatively self documenting. And tools like GraphiQL can leverage schema data to preset you with documentation for your project. On top of that, we have a standardized JSON format for responses defined for us up front, we don't need to worry about how many different request formats we're going to end up with. And we can modify these requests to grow and change as our data grows and changes. Adding in that "private" post feature? Just throw a private: Bool! on the type Post and you're good to go. All of your queries can now start leverage this new data.

If your interest has been piqued I would encourage to read the GraphQL spec, or dive into a library for your favorite language and spin up a quick and easy GraphQL server. There are tons of tutorials around by now.

I think it's important to end on a note on when I think GraphQL is not the answer. As you may have potentially noticed, GraphQL hinges on a predefined schema. Most libraries I've used involve a relatively static Schema definition at runtime and honestly I think that should be honored. If you're working in a way to dynamically add fields to your schema at runtime, that's a symptom that GraphQL is not the answer to your problem. Likewise if you're building something that takes on a large set of unknown or dynamic data (like a CMS with a map of user defined keys/values) that also doesn't really fit into the problem space of GraphQL. You can define scalars, and you can handle this if you really want to but I don't recommend it. My recommendation is to supplement with a REST endpoint to fetch that dynamic data. It's fine to return a URL, or ID of some kind instead of the object in GraphQL and follow up with a REST call -- so don't try and force everything you need into GraphQL. Remember, not all problems are nails so this hammer will not not work on the non-nail problems. At least no how you might want.

I hope this has provided some insight into the REST vs. GraphQL debate. While it may not be perfect for everything, I think GraphQL is a great solution to 99% of the problems we face writing JSON APIs.