GraphQL Is Dead, Long Live JSON:API (In Rails)

July 29, 2019

Before I get started, I’d like to start with a simple disclaimer. GraphQL is not a terrible solution or technology. In my opinion, however, it sacrifices quite a lot of features and functionality that a RESTful API gives you. It does this in the name of flexibility, but here I’ll demonstrate to you that flexibility is not something sacrificed by using a JSON:API approach. Without further ado, let’s get into it.

Note: This article does assume at least some base knowledge of GraphQL and API Design.

The Setup

Imagine you were tasked with building an API for Basecamp. They wanted you to expose an interface for projects. Users need to authenticate to get a list of projects and projects have members, tasks, documents, and posts. The first feature requested is to allow users to fetch all of their projects and associated data.

Quering Data - Broad Strokes

Assuming the schema is already in place with all proper types defined, the query might look something like we have below.

query {
  projects(accountId: 1) {
    id
    name

    projectMembers {
      user {
        id
        name
      }
    }

    todos {
      content
      complete

      assignee {
        name
      }
    }

    documents {
      name
      versionNumber
      url
    }
  }
}

Using JSON:API with something like Graphiti the request would look like this:

curl https://localhost:3000/api/v1/projects?include=project_members,project_members.user,todos,todos.assignee,documents&filter[account_id][eq]=1&fields[container]=id,name&fields[user]=id,name&fields[todos]=content,complete&fields[documents]=name,version_number,url \
    -H "Content-Type: application/vnd.api+json" \
    -H "Accept: application/vnd.api+json"

The results returned by both APIs are equivalent. Interested in hearing more? Read on. Fetching data is probably where GraphQL shines the most because it allows a client to specify objects, relationships, and “sparse fieldsets”. However we can see above that JSON:API can do that too, albeit with slightly less readability. Of course, if we executed the GraphQL request with curl we would see something pretty similar to our JSON:API situation.

Writing Data - Broad Strokes

Let’s talk about writing/persisting data. This is probably the most interesting comparison. We start again by looking at how to do this with GraphQL.

We start by defining the allowed “mutation”.

mutation CreateTodoForProject($projectId: Integer!, $content: String!, $assigneeId: Integer, $completed: Boolean) {
  createTodo(projectId: $productId, content: $content, assigneeId: $assigneeId, completed: $completed) {
    projectId
    content
    completed
    assigneeId
  }
}

And then we make our request:

{
  "data": {
    "createTodo": {
      "projectId": 1,
      "content": "This is great content",
      "completed": false,
      "assigneeId": 2
    }
  }
}

Well isn’t this interesting..? In order to mutate our data we have to tell GraphQL how to allow it. That seems cumbersome, I wonder how JSON:API handles it.

{
  "data": {
    "type": "projects",
    "id": 1,
    "relationships": {
      "todos": [{ "type": "todos", "temp-id": "1233345", "method": "create" }]
    }
  },
  "included": {
    "temp-id": "1233345",
    "type": "todos",
    "attributes": {
      "assignee_id": 2,
      "content": "This is great content",
      "completed": false
    }
  }
}

What I find powerful about this example is the lack of work needed to support it. Frameworks like Graphiti or JSONAPI:Resources understands your data hierarchy based on the relationships you define. Once you have that done you can write simple POST requests or complex nested changes. There is no need to maintain mutations and error messages are returned where they are relevant. If the request above was invalid, the response might look something like:

{
  "errors": [{
    "code": "unprocessable_entity",
    "status": "422",
    "title": "Validation Error",
    "detail": "content can't be blank",
    "source": { "pointer": "/data/attributes/content" },
    "meta": {
      "relationship": {
        "attribute": "content",
        "message": "can't be blank",
        "code": "blank",
        "content": "",
        "id": "1233345",
        "type": "todos"
      }
    }
  }]

This is pretty nice! We can perform our nested writes and if anything fails you know right where it did. Still not convinced? Ok, let’s take a look at how GraphQL might handle this same situation.

{
  "errors": [{
    "message": "Request is invalid.",
    "path": ["createTodo"],
    "state": {
      "content": ["can't be blank"]
    }
  }]
}

Hmmm, this is a bit less awesome. We can see that our operation failed, but we don’t have a good correlation between our proposed todo object and these errors. This means that on a fundamental level your errors are tied to mutations and not necessarily tied to the objects you are trying to mutate. It makes things like rendering complex nested forms pretty difficult. If you want to read a bit more about mutations and validation check out this excellent article on medium.

GraphQL and Ruby - The Bad Parts

Especially in Ruby land, things are not quite where you want them to be. If you want your API to scale well, handle complex authorization schemes, or cache your responses your will have to roll your own or pay for them. Right now GraphQL Pro seems to be the most popular solution to these concerns. If you don’t want to pay you can find an open source caching gem and this gem from Shopify that aims to make batch operations less painful.

It’s really easy to latch onto new and exciting and even popular tech. The great folks at Facebook have done an amazing amount of work to create GraphQL and maintain pretty excellent docs to boot. However, GraphQL isn’t the only way to build APIs right now, especially not in Rails. It may not even be the best way. For you more full-stack folks I know what you are thinking…there are lots of tutorials out there on Apollo with React and lots of folks are building things this way. But I encourage you to push past the popularity. Some folks are already hard at work publishing good client libraries to consume JSON:API services. Check these out:

Hopefully, I haven’t upset any of you folks who have stuck around to read this engineer’s opinion. I’m currently building a JSON:API on Rails with Graphiti that I plan to share with you soon. We will go more in-depth into some of the topics we brushed through on here. After that, we will dive into the client-side and build a modern React application that consumes our API. I’m really excited to show how great building APIs on this tech stack can be. Cheers!

Content to Come

  • JSON:API on Rails Part 1
  • JSON:API on Rails Part 2
  • GraphQL on Rails Part 1
  • GraphQL on Rails Part 2
  • React with JSON:API
  • Vue with JSON:API

Comments

comments powered by Disqus