Designing a REST API isn’t easy. Anyone who claims differently either hasn’t designed one, hasn’t designed one for a moderately complex system, or is a rare genius who has never found anything to be challenging in their lives. One can set out determined to adhere to REST design principles where a client user can intuitively find the endpoints they need, where all “objects” in the system are represented as resources that can be acted upon with one of four HTTP operations (POST, GET, PUT, DELETE) and users can do whatever they’re trying to achieve with as few calls as possible. But challenges will appear. Immediately.
A couple years ago at Jama, we set out to build a new REST API to eventually replace all our SOAP and DWR interfaces. Two key design challenges were:
- Chatty vs. Chunky API Design
- Modeling Resources vs. Business Processes
The second challenge is difficult, and it’s one I could talk about all day (and perhaps I will in a later post). But, today I’m going to focus on the Chatty vs. Chunky problem.
Chatty vs. Chunky APIs
It wasn’t long after people started writing against our initial iteration of the API, that the conversation about “chattiness” came up. API developers have been having conversations about this concept all over the web, but the essence of it is this: Users need to get the data they are after in as few calls as possible. If they have to make too many smaller calls to get all the data they seek, then the API is too “chatty” for their needs. On the other hand, if API calls are too large, and return more data than is needed, the API can be considered too “chunky”. The two ends of this spectrum are also referred to as Fine Grained vs Coarse Grained APIs.The user call would look something like this:
Let’s give an example to illustrate this. Say I have an API call to retrieve user comments on a blog. Comments can be made on a blog post, so, if they are, they should have a page property to indicate the page they appear on. They should also have an author field to indicate who made the comment.
The user call would look something like this:
GET /comments?page=27
And the JSON data in the response might look something like this:
[
{
"id": "796",
"author": "23",
"page": "27",
"createdDate": "2016-04-08T09:15:00",
"text": "This blog page changed me for the better. I've never read anything quite like it."
},
{
"id": "1097",
"author": "1",
"page": "27",
"createdDate": "2016-04-08T16:15:00",
"text": "I agree! Thanks for posting!",
"inReplyTo": "796"
}
]
This example is overly simplified, but you can see in the comment, the author properties have the values 23
and 1
which you can assume are the authors’ unique user IDs. Similarly, the blog page these comments are on is being referred to by its page ID of 27
. The second comment is also in reply to some other person’s comment, so that inReplyTo
property has a value of 796
to reference another comment ID.
This payload is very short and simple, but it presents some problems if the API user is interested in knowing more about the author than just the author’s user ID (you can be sure that they at least want to know the author’s name!)
First, if you’re the user of this API, you’re not given any indication of how to retrieve the complete user information associated with user ID 1
, nor the complete information for what content is on page ID 27
. This is a problem with “discoverability.”
But even if that discoverability problem is solved, you would still need to make a separate API call to retrieve that user. Further, if you are retrieving a collection of hundreds of comments, you would potentially need to make a user call for each comment you retrieve. You would need to make hundreds calls to the API to get the information you’re looking for. Hence the term “chatty”.
But let’s look at the other end of the spectrum. The API could attach the complete information of all object properties to the response and you’d get something like this:
[
{
"author": {
"id": "23",
"active": "true",
"firstName": "Lisa",
"lastName": "Turtle",
"avatarUrl": "http://base_url.com/lisa.jpg",
"registrationDate": "2012-04-19T09:16:00",
"hobbies": "It's turtles all the way down"
},
"page": {
"id": "27",
"title": "The Meaning of Life",
"createdDate": "2016-04-07T14:07:00",
"author": {
...another user object...
}
...and so on...
},
"createdDate": "2016-04-08T09:15:00",
"text": "This blog page changed me for the better. I've never read anything quite like it."
},
{
"author": {
"id": "1",
"active": "true",
"firstName": "Jason",
"lastName": "Goetz",
"avatarUrl": "https://www.jamasoftware.com/app/uploads/2016/04/FEAT-Jason.jpg",
"registrationDate": "2004-02-19T07:16:00",
"hobbies": "Public debate, dancing, skeet shooting"
},
"page": {
"id": "27",
"title": "The Meaning of Life",
"createdDate": "2016-04-07T14:07:00",
"author": {
...another user object...
}
...and so on...
},
"createdDate": "2016-04-08T16:15:00",
"text": "I agree! Thanks for posting!",
"inReplyTo": {
...the first comment data repeated again?...
}
}
]
As the API user, you now have all the information you need. The full author information is available, you know the full details of the blog page that the comment was posted on, and you can even see the entire other comment that this comment was in reply to directly in the inReplyTo
value.
But you have a myriad of new problems.
The first is the sheer size of the payload returned. This is a fairly simplified example. User and page objects would likely have many more properties than these examples show. If you are retrieving hundreds of comments and you’re getting a full object for every property on each comment, this is going to be a lot of data. As the client user, you may not have bandwidth concerns about getting this much data across the wire, but it certainly could take the server more time to assemble all that data, and long-running transactions are much harder on a server’s CPU & memory. Especially when the server is dealing with multiple concurrent requests.
It should also be noted, if it turns out that 99 out of 100 retrieved comments were all authored by the same user and all the comments are posted on the same page, then most of the user and page objects in your results are going to be redundant. The time the server spent assembling user and page data was mostly wasted.
Data inconsistencies are also bound to come up. In this case, what if there aren’t any restrictions on how many embedded replies you can have in your comments section? If someone replies to the first comment, then someone replies to that reply, then someone replies to that reply… you get the point. How should that data be represented? You could choose to go one level deep but API users may be confused about the point at which the API decides to cut off the addition of further data, and how they should write a client to consume it.
These are the problems with a chunky API.
The API could also try to find some kind of compromise and attach only partial data. While the full author’s info may be retrievable from some user call, the comment payload may only contain the author’s user ID, first name, and last name.
But, there are problems with this approach as well (this all sounds so negative!). Inconsistent partial objects make it harder to intuitively work with the API. The same inconsistencies described with the inReplyTo example above apply here as well. Also, the API may not provide the data you are looking for in the first place. If you just want the author’s name and avatar, but the avatar isn’t provided, you’ll still need to make a separate call to get the full user object just so you have that data.
So, what’s the best approach then?
The Solution
When making design decisions and facing a spectrum like this where both ends of the spectrum provide their own challenges, we can only strive to find balance and be practical. We need to find a solution to the Chatty API design problems while avoiding going down the Chunky API route. We want to remain simple, clean, and RESTful. We also want to make our API flexible enough to allow users to meet their own needs in the chatty to chunky continuum.
While the “partial data” example above attempts to find a compromise between Chatty and Chunky, I’ve already pointed out some issues with a compromised approach. Instead of compromising, what if we instead adhere to everything we like about the chatty API model, but give users the extra facilities to add data to their response?
Let’s look at another approach. With this approach, the calling user makes a request for comments, but specifies they would like the author field included as well:
GET /comments?page=27&include=data.author
The response would look like this:
{
"links": {
"data.author": {
"type": "user",
"href": "http://base_url.com/comments/{data.author}"
},
"data.page": {
"type": "page",
"href": "http://base_url.com/comments/{data.page}"
},
"data.inReplyTo": {
"type": "comment",
"href": "http://base_url.com/comments/{data.inReplyTo}"
}
},
"linked": {
"user": {
"1": {
"id": "1",
"active": "true",
"firstName": "Jason",
"lastName": "Goetz",
"avatarUrl": "https://www.jamasoftware.com/app/uploads/2016/04/FEAT-Jason.jpg",
"registrationDate": "2004-02-19T07:16:00",
"hobbies": "Public debate, dancing, skeet shooting"
},
"23": {
"id": "23",
"active": "true",
"firstName": "Lisa",
"lastName": "Turtle",
"avatarUrl": "http://base_url.com/lisa.jpg",
"registrationDate": "2012-04-19T09:16:00",
"hobbies": "It's turtles all the way down"
}
}
},
"data": [
{
"id": "796",
"author": "23",
"page": "27",
"createdDate": "2016-04-08T09:15:00",
"text": "This blog page changed me for the better. I've never read anything quite like it."
},
{
"id": "1097",
"author": "1",
"page": "27",
"createdDate": "2016-04-08T16:15:00",
"text": "I agree! Thanks for posting!",
"inReplyTo": "796"
}
]
}
This is a lot to ingest at once, but the payoff is worth it. You can see here that the comments payload is now listed under data
. Also, there are now separate properties in the response called links
and linked
.
The data
section is exactly the same as the one given under the Chatty API example above. But, with the helpful links
and linked
properties, the lack of associated data is much less of an issue.
The links
section takes care of the “discoverability” problem I mentioned above. For any property that simply displays an ID (like author
, page
and inReplyTo
) the links section will describe how you can plug that ID into a separate API call to retrieve the information you’re seeking.
The linked
section is where we really begin to solve the problems associated with Chatty APIs. In the request, you have asked to include any user
objects that are referenced in any of the comment author
fields. The resulting response now gives a data store of user
objects in the linked
section for any user IDs specified under author
. This removes the need to make any further calls to the users
endpoint to get all the information you’re seeking. But it also solves the redundancy problems since a user will only appear once per user ID. In other words, you could have 99 comments where the author
value is the same, but you’d only have one inclusion of that author’s data in the linked
section. This also (potentially) takes less time for the server to assemble than a full attachment of all author
data to each individual comment since we’re only loading and assembling the author
data once for the data store.
This solution offers the simplicity of the chatty API at its base. It’s only giving you the basic information you’re requesting. But, it additionally gives you discoverability and the flexibility to ask for further data in the same request so we solve the main problems associated with chatty APIs. We’ve managed to address all of these previously mentioned problems:
- Discoverability
- Not enough data i.e. the need for repeated API calls or chattiness
- Large payloads associated with chunky APIs
- Server processing time associated with chunky APIs
- Redundancy
Here at Jama, while designing our REST API, we’ve set out to find balance in our API design with an emphasis on ease-of-use, practicality and flexibility for our users. This chatty vs chunky tradeoff is just one aspect of the challenges we face to build an API that works for us and our users. We’ve come up with a REST JSON response data structure very similar to the one in the example above. It allows us to be uncompromising in resources being clean and lean, while still allowing our users to retrieve the data they seek. It’s loosely based on an initial version of the JSON API specification and we feel it elegantly satisfies our and our users’ needs.
Comments? Questions? I’d love to hear your feedback!
- REST API Design - April 27, 2016
- Jama REST API: Officially Open to Developers - October 20, 2015
This approach is commonly used by many other cloud APIs. One more problem that I seek is in supporting multiple devices from rest API. Let say you have the liberty to show many sections of information in a web page and for that you are making different calls to your backend services to render home page. Whereas for mobile applications you may not be interested in showing plethora of contents. Having just field selection feature will still not be enough to control the number of calls you are making at backend.
Interesting article. I’ll remark that OSLC with its standardized conventions for discoverability, for the “shaping” of properties into Resources, and with its OSLC Query Language that provides a SQL-like or SPARQL-like “Select From Where Limit Offset Order By ” grammar for its REST URLs seems to address both of these Chatty / Chunky issues. It does it in a standardized way.