A RESTful API exposes a list of resource representations. This architecture often creates APIs that are quite close to the business model. While this can be fine to avoid duplication, it forces clients of the APIs to make several requests in order to get all the data they need to fulfill a given task. On mobile networks, where latency is constantly high, this is a big drawback. So how can you design an API following REST principles, without forcing the clients to make too many requests?
Let's take an example: a mobile bookstore application offers a search feature, where the user can look for books by author name. The mobile application uses a REST API provided by a bookstore web service, exposing data on authors and books. Here is the list of API calls the application needs to show the list of books written by authors named "Tol*":
GET /authors?name=Tol* | |
HTTP/1.1 200 OK | |
[ | |
{ id: 1234, name: "Leo Tolstoy" }, | |
{ id: 5678, name: "J. R. R. Tolkien" } | |
] | |
GET /authors/1234/books | |
HTTP/1.1 200 OK | |
[ | |
{ id: 6789, name: "War and Peace", publicationDate: 1869 }, | |
{ id: 7564, name: "Anna Karenina", publicationDate: 1877 } | |
] | |
GET /authors/5678/books | |
HTTP/1.1 200 OK | |
[ | |
{ id: 5432, name: "The Hobbit", publicationDate: 1937 }, | |
{ id: 7312, name: "The Lord of the Rings", publicationDate: 1955 } | |
] |
The data set is simplified, but you get the idea: the number of HTTP requests necessary to build the response on the application side is proportional to the number of responses to the first request.
If the application was talking with a database, the solution would be simple: use a JOIN to retrieve data from the book
table associated with the results from the author
table. But this is REST, not SQL, and there is no such thing as join. What convention would you follow to allow denormalization of related results?
REST is not a normalized standard, and only answers partially to the requirements of web service design. There has been other attempts at normalizing data exchange between servers. One of them, backed by Microsoft, is called OData, or Open Data Protocol. OData goes way beyond the simple architectural principles of REST, and gives a convention to the solve the denormalization problem: $expand
. Examples from the OData documentation show this in practice:
http://services.odata.org/OData/OData.svc/Categories?$expand=Products http://services.odata.org/OData/OData.svc/Categories?$expand=Products/Suppliers
The $
prefix is another convention from the OData protocol to denote "System Query Options". It is widely used to provide filtering, ordering and more, somehow following the SQL path. For instance, an OData HTTP request can look like this:
http://services.odata.org/Northwind/Northwind.svc/Customers?$filter=tolower(CompanyName) eq 'foobar' &select=FirstName,LastName&$orderby=Name desc&$format=json
This is not a RESTful representation (note the &$format=json
), and it's probably exposing too much filtering options for a simple REST service. But the $expand
option fits the need.
Some true RESTful web services providers also offer an expand
option. NetFlix, for instance, uses it for title expansion. An example from the NetFlix documentation is shown below:
http://api-public.netflix.com/catalog/titles/series/70023522?expand=cast,directors
JIRA uses the same convention for its REST API:
https://jira.atlassian.com/rest/api/latest/issue/JRA-9?expand=names,renderedFields
Following the expand
convention to do a JOIN equivalent in a REST API seems like a good idea. So, for the bookstore example, that would give the following:
GET /authors?name=Tol*&expand=books | |
HTTP/1.1 200 OK | |
[ | |
{ id: 1234, name: "Leo Tolstoy", books: [ | |
{ id: 6789, name: "War and Peace", publicationDate: 1869 }, | |
{ id: 7564, name: "Anna Karenina", publicationDate: 1877 } | |
] }, | |
{ id: 5678, name: "J. R. R. Tolkien", books: [ | |
{ id: 5432, name: "The Hobbit", publicationDate: 1937 }, | |
{ id: 7312, name: "The Lord of the Rings", publicationDate: 1955 } | |
] } | |
] |
And now the mobile application only needs one HTTP request to retrieve all the necessary information to display the search results page.
Note that the expand parameter is not really a consensus. Some architects suggest using the Accept HTTP header to achieve the same goal.
You're probably aware that designing URIs like /authors/1234/books
isn't sufficient to make an API RESTFUL. To reach the glory of REST and get to level 3 of Richardson Maturity Model, you must achieve Hypertext As The Engine Of Application State (HATEOAS). Check David Zuelke's excellent presentation about RESTful web services for details.
Let's switch to XML as there is now a broadly accepted convention on implementing HATEOAS in this language (there is not yet one in JSON). It may be more verbose than JSON, but every client library can decode XML natively nowadays. The initial REST API, exposing resources using HTTP verbs, reaches Level 2 of the RMM:
GET /authors?name=Tol* | |
HTTP/1.1 200 OK | |
<?xml version="1.0" encoding="utf-8" ?> | |
<authors> | |
<author id="1234"> | |
<name>Leo Tolstoy</name> | |
</author> | |
<author id="5678"> | |
<name>J. R. R. Tolkien</name> | |
</author> | |
</authors> | |
GET /authors/1234/books | |
HTTP/1.1 200 OK | |
<?xml version="1.0" encoding="utf-8" ?> | |
<books> | |
<book id="6789"> | |
<name>War and Peace</name> | |
<publicationDate>1869</publicationDate> | |
</book> | |
<book id="7564"> | |
<name>Anna Karenina</name> | |
<publicationDate>1877</publicationDate> | |
</book> | |
</books> | |
GET /authors/5678/books | |
HTTP/1.1 200 OK | |
<?xml version="1.0" encoding="utf-8" ?> | |
<books> | |
<book id="5432"> | |
<name>The Hobbit</name> | |
<publicationDate>1937</publicationDate> | |
</book> | |
<book id="7312"> | |
<name>The Lord of the Rings</name> | |
<publicationDate>1955</publicationDate> | |
</book> | |
</books> |
HATEOAS suggests using links to allow clients to discover locations and operations. That way, URLs can change without breaking every client application.
An author
resource should therefore expose links about itself, and related resources:
<author id="1234"> | |
<name>Leo Tolstoy</name> | |
<link rel="self" href="/authors/1234" /> | |
<link rel="books" href="/authors/1234/books" /> | |
</author> |
How does expand
fit in this syntax? Just like NetFlix, you should expand the <link>
tag using the same rel
attribute as the expand
parameter value, and put the related data inside it:
GET /authors?name=Tol*&expand=books | |
HTTP/1.1 200 OK | |
<?xml version="1.0" encoding="utf-8" ?> | |
<authors> | |
<author id="1234"> | |
<name>Leo Tolstoy</name> | |
<link rel="self" href="/authors/1234" /> | |
<link rel="books" href="/authors/1234/books"> | |
<books> | |
<book id="6789"> | |
<name>War and Peace</name> | |
<publicationDate>1869</publicationDate> | |
<link rel="self" href="/books/6789" /> | |
</book> | |
<book id="7564"> | |
<name>Anna Karenina</name> | |
<publicationDate>1877</publicationDate> | |
<link rel="self" href="/books/7564" /> | |
</book> | |
</books> | |
</link> | |
</author> | |
<author id="5678"> | |
<name>J. R. R. Tolkien</name> | |
<link rel="self" href="/authors/5678" /> | |
<link rel="books" href="/authors/5678/books"> | |
<books> | |
<book id="5432"> | |
<name>The Hobbit</name> | |
<publicationDate>1937</publicationDate> | |
<link rel="self" href="/books/5432" /> | |
</book> | |
<book id="7312"> | |
<name>The Lord of the Rings</name> | |
<publicationDate>1955</publicationDate> | |
<link rel="self" href="/books/7312" /> | |
</book> | |
</books> | |
</link> | |
</author> | |
<link rel="self" href="/authors?name=Tol*&expand=books" /> | |
</authors> |
Note the use of the <link rel="self">
tag even on book
to always reference the canonical URL for a given resource.
Expanding one sub-element is quite simple, but how would you ask for a list of authors embedding author books, reviews and sales on author books, and biographical data on authors? The expand
parameter can accept several fields separated by commas (","), and the object hierarchy can be traversed using dot notation ("."). So the request would look like:
GET /authors?name=Tol*&expand=books,books.reviews,books.sales,bioData
With this convention, virtually every page of a mobile application can be reconstituted based on the response from one single request to the API, minimizing network traffic and latency. It's the API application responsibility to know what type of JOIN and expand
must be translated to.
REST and Mobile are not enemies. Instead of leaving REST to craft your own custom denormalized API for mobile applications, offer the expand
option. That way, both web and mobile may use the same API, with optimal performance. As for the implementation, as long as relationships between model objects are clearly defined in your code, it's a piece of cake. And if you happen to use an ORM or an ODM, offering the expand
options won't be a problem.
Tweet
Published on 09 Aug 2012
with tags development rest