Tuesday, April 5, 2011

What's missing from our REST Services?

While reading the excellent book RESTful Web Services I discovered something. A technique used on the web since its beginning. Twitter and Facebook seem to use it. Yet it was absent in the REST Services I had been developing and if I had to guess most REST developers aren't including it either. What is so valuable the Internet would be useless without it? The answer is hypermedia.

Hypermedia is the technical term used to describe how web pages (resources) are linked or connected. The author of RESTful Web Services calls it connectedness: how the server guides the client from one application state to another by providing links. A good example of a well-connected application is Wikipedia. It's very powerful when you could pick a random page and click through every entry on Wikipedia without ever editing your browsers URL. Performing a search on Google is another great example, as it wouldn't be very useful if the search result didn't include any links. Without these links, the client must know and create predefined rules to build every URL it wants to visit.

So what do links on a website have to do with REST Services? REST was built on the foundation of the web and just because your not returning HTML doesn't mean you shouldn't create relationships between your resources. In fact, it's amazing how powerful embedding links in your responses can be. For instance, it prevents tight coupling between your clients and services as the clients don't have to construct or even know the exact URLs because the services are providing them.

Let's use Twitter as an example. Assume the following is the URL to get the 20 most recent tweets for my account:

http://api.twitter.com/1/statuses/user_timeline.json?screen_name=jlorenzen

And here is a shortened fictitious response:

[
    {
        "text": "Finished watching Battlestar Galatica"
        "id": "55947977415667712"
        "url": "http://api.twitter.com/1/statuses/show/55947977415667712.json"
    },
    {
        "text": "Started watching Battlestar Galatica"
        "id": "45947977415667712"
        "url": "http://api.twitter.com/1/statuses/show/45947977415667712.json"
    }
]

Notice each tweet includes a direct URL. Now clients can use that URL value verses constructing it, and if it changes in the future, clients don't have to make any changes.

Example using Jersey
So what is the best way to include links in your REST Services? Since I use Jersey on a daily basis, I'll go ahead and show an example of how to embed links in your responses using Jersey. Most likely, any REST framework is going to provide the same kind of features.

Using the twitter example above, the User Status Service with links may look something like this (using groovy):

import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.Produces
import javax.ws.rs.QueryParam
import javax.ws.rs.core.Context
import javax.ws.rs.core.UriInfo

import com.test.UserStatuses

@Path("/statuses")
class StatusesResource {
 
    @Context 
    UriInfo uriInfo

    @GET
    @Path("/user_timeline")
    @Produces(["application/json", "application/xml"])
    def UserStatuses getUserTimeline(@QueryParam("screen_name")String screen_name) {
        def statuses = getStatuses(screen_name)
        
        statuses.each {
            it.url = "${uriInfo.baseUri}statuses/show/$it.id"
        }
        
        return new UserStatuses(statuses: statuses)
    }
}

In this simple example, Jersey injects the UriInfo object which we use to get the baseUri of the request. It's really that simple.


Potential Issues
Well for some it may not be that simple. For example, we discovered an issue when using a reverse proxy (apache httpd). In our production environments, we typically setup apache on port 80 to proxy a localhost JBoss on port 8080. Unfortunately, in this setup the UriInfo.getBaseUri() returns localhost:8080 and not the actual original URL the client used; which is obviously not good. Now if you don't use a reverse proxy then no worries. However, if you do or might potentially in the future, a easy solution seems to be to set the Apache Proxy module option ProxyPreserveHost to On. Setting this to On and restarting Apache seems to fix the issue.

JSONView Firefox Plugin
Once you've started embedding URLs in your REST responses, you might find it useful to install the JSONView firefox plugin. It's got some really slick features like formatting the JSON and creating clickable links for URLs.