1 Comment

Leveraging the Boundary Between Client and Server in a REST API

I recently encountered an interesting problem while sketching out a RESTful API for a side project with the JSON API specification. I’m definitely not the first person to run into this problem, but it ended up being a great thought exercise for designing APIs and better understanding the client-server relationship.

The Simplified Story

Consider a back end whose domain layer models three types of entities: Magazines, Articles, and Authors. The back-end server supplies an API, and the client has a view that:

  • Displays information about a Magazine
  • Lists Articles recently published in that Magazine, the subviews for those Articles, and some information about the Author

Using standard ownership relations for these models, a Magazine has many Articles, an Author has many Articles, and an Article belongs to one Magazine and one Author. When the client makes a request for the data used to render the view, it needs a response that looks something like this:

magazine: {
  name: "Magazine",
  articles: [
    {
      title: "Some Article",
      date: "10/1/2017",
      author: {
        name: "Some Author",
        title: "Correspondent"
      }
    }
  ]
}

The Problem

Notice that there are two levels of embeddedness here, which isn’t really that many. Using the JSON API standard, every entity would actually be available at the top level, and embedded objects would reference their entities by ID.

But conceptually, there would still be two levels of embedding. And because each of the entities (hypothetically) has fields other than the ones required by the client, the most efficient GET request would look something like this:

GET /magazines/20?include=article,article.author&fields[magazine]=name&fields[article]=name,date&fields[author]=name

That’s pretty verbose if you ask me, and the actual use case required an even more complex query.

This solution is so verbose that I wouldn’t use it, and that’s essentially what inspired this post: REST seemed to be failing me by introducing complexity when I needed embedded resources. Though I really should have done some critical thinking off the bat, instead, I went through a small exploration of possible alternative API specifications.

GraphQL

There’s a lot of excitement about GraphQL right now. I’ve read about it and understand the appeal, but I haven’t actually used it before. At first, it seemed like a viable solution for my problem because the query closely resembles the response object described above:

{
  magazine(id: 20) {
    title,
    date,
    author: {
      name,
      title
    }
  }
}

That’s still verbose, but I think it’s easier to read than the JSON API query.

However, GraphQL also seemed like a bit of an overhaul. It would require adding some infrastructure (like schema definitions) to the back end, and of course, the overhead of learning the ins and outs of the specification.

The client for my project mostly does CRUD operations on individual resources (at least in this early stage of development), so this particular Magazine view is an exception that doesn’t merit re-designing the whole API system. While I’d love to build a GraphQL API sometime soon, this was not the most opportune moment.

Multiple API Calls

I also considered splitting the API call into two separate calls: one to get the Magazine entity, and one for the list of Articles and their embedded Authors. This would break up the complexity of the original query, but it obviously had a number of downsides.

Twice as many network requests wouldn’t be worth the simplification. The overall query complexity wouldn’t really be reduced, just split into parts. Plus, the client logic would actually become more complicated, since it would have to handle two responses. Obviously, this solution was not a serious contender.

Remote Procedure Calls

The next solution I considered was an RPC API. This concept was new to me, but it made sense. Think of the server as another layer of the client, where the payload of an API call encapsulates the name and parameters of a procedure. This means the API call would look something like:

{
  name: "getMagazinePreview",
  id: "20"
}

This was definitely more simple than either the original JSON API or GraphQL queries. It also decoupled the client and server, because no fields were explicitly included or excluded. Decoupling has pros and cons, but here, I’d consider it a good thing.

So far, I preferred the RPC option for its semantic precision, but it seemed fishy that I was just prepending the name of the resource I wanted with “get.” This made it feel more like an accessor than a procedure.

This was where things started to come together. My goal was to access/get/read a Magazine preview, but “Magazine Preview” wasn’t something that was specifically modeled in my domain layer. In fact, a Magazine Preview would just be a simple composition of entities that are modeled in the domain. And most importantly, I could treat it as a regular read-only resource, which led to:

GET /api/magazine_preview/20

Perfect. As long as this “resource” had only one GET endpoint, it fit pretty naturally into the REST pattern. The query was even less verbose than the RPC version, but it was the most semantically accurate so far. Best of all, it didn’t conflict with the JSON API pattern I wanted to adopt in the first place.

The Takeaways

In retrospect, the domain layer had been clouding my vision of the overall application because I was only thinking in terms of existing models. The solution was to abstract away from these models at the API layer in order to accommodate the client application.

This is a very convenient place to introduce abstraction because the client is completely unconcerned with how these resources are built, and the server only has to model a “virtual” entity like the Magazine Preview in the view layer.

There’s also a more general takeaway: After researching a number of alternatives to a RESTful design, the best solution was simply to use REST more correctly. Creating one endpoint for a virtual resource turned out to be elegant and efficient. Instead of wandering through API land, I should have stopped myself to think about how the strengths of JSON API could be leveraged to solve my problem.