HATEOAS for a Spring REST Service

Table of Contents

1. Overview

This article will focus on the implementation of discoverability in a Spring REST Service and on satisfying the HATEOAS constraint.

2. Decouple Discoverability through events

Discoverability as a separate aspect or concern of the web layer should be decoupled from the controller handling the HTTP request. In order to do so, the Controller will fire off events for all the actions that require additional manipulation of the HTTP response.

First, the events:

public class SingleResourceRetrieved extends ApplicationEvent {
    private HttpServletResponse response;

    public SingleResourceRetrieved(Object source, 
      HttpServletResponse response) {
        super(source);

        this.response = response;
    }

    public HttpServletResponse getResponse() {
        return response;
    }
}
public class ResourceCreated extends ApplicationEvent {
    private HttpServletResponse response;
    private long idOfNewResource;

    public ResourceCreated(Object source, 
      HttpServletResponse response, long idOfNewResource) {
        super(source);

        this.response = response;
        this.idOfNewResource = idOfNewResource;
    }

    public HttpServletResponse getResponse() {
        return response;
    }
    public long getIdOfNewResource() {
        return idOfNewResource;
    }
}

Then, the Controller, with 2 simple operations – find by id and create:

@Controller
@RequestMapping(value = "/foos")
public class FooController {

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Autowired
    private IFooService service;

    @RequestMapping(value = "foos/{id}", method = RequestMethod.GET)
    @ResponseBody
    public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
        Foo resourceById = Preconditions.checkNotNull(service.findOne(id));

        eventPublisher.publishEvent(new SingleResourceRetrieved(this, response));
        return resourceById;
    }

    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    public void create(@RequestBody Foo resource, HttpServletResponse response) {
        Preconditions.checkNotNull(resource);
        Long newId = service.create(resource).getId();

        eventPublisher.publishEvent(new ResourceCreated(this, response, newId));
    }
}

These events can then be handled by any number of decoupled listeners, each focusing on it’s own particular case and each moving towards satisfying the overall HATEOAS constraint.

The listeners should be the last objects in the call stack and no direct access to them is necessary; as such they are not public.

3. Make the URI of a newly created Resource discoverable

As discussed in the previous post on HATEOAS, the operation of creating a new Resource should return the URI of that resource in the Location HTTP header of the response; this is handled by a listener:

@Component
class ResourceCreatedDiscoverabilityListener
  implements ApplicationListener< ResourceCreated >{

    @Override
    public void onApplicationEvent( ResourceCreated resourceCreatedEvent ){
       Preconditions.checkNotNull( resourceCreatedEvent );

       HttpServletResponse response = resourceCreatedEvent.getResponse();
       long idOfNewResource = resourceCreatedEvent.getIdOfNewResource();

       addLinkHeaderOnResourceCreation( response, idOfNewResource );
   }
   void addLinkHeaderOnResourceCreation
     ( HttpServletResponse response, long idOfNewResource ){
       URI uri = ServletUriComponentsBuilder.fromCurrentRequestUri().
         path("/{idOfNewResource}").buildAndExpand(idOfNewResource).toUri();
       response.setHeader( "Location", uri.toASCIIString() );
    }
}

We are now making use of the ServletUriComponentsBuilder – this was introduced in Spring 3.1 to help with using the current Request. This way, we don’t need to pass anything around and we can simply access this statically.

If the API would return ResponseEntity – we could also use the Location support introduced here.

4. Get of single Resource

On retrieving a single Resource, the client should be able to discover the URI to get all Resources of that particular type:

@Component
class SingleResourceRetrievedDiscoverabilityListener
 implements ApplicationListener< SingleResourceRetrieved >{

    @Override
    public void onApplicationEvent( SingleResourceRetrieved resourceRetrievedEvent ){
        Preconditions.checkNotNull( resourceRetrievedEvent );

        HttpServletResponse response = resourceRetrievedEvent.getResponse();
        addLinkHeaderOnSingleResourceRetrieval( request, response );
    }
    void addLinkHeaderOnSingleResourceRetrieval ( HttpServletResponse response ){
        String requestURL = ServletUriComponentsBuilder.fromCurrentRequestUri().
          build().toUri().toASCIIString();
        int positionOfLastSlash = requestURL.lastIndexOf( "/" );
        String uriForResourceCreation = requestURL.substring( 0, positionOfLastSlash );

        String linkHeaderValue = LinkUtil
          .createLinkHeader( uriForResourceCreation, "collection" );
        response.addHeader( LINK_HEADER, linkHeaderValue );
    }
}

Note that the semantics of the link relation make use of the “collection” relation type, specified and used in several microformats, but not yet standardized.

The Link header is one of the most used HTTP header for the purposes of discoverability. The utility to create this header is simple enough:

public final class LinkUtil {
    public static String createLinkHeader(final String uri, final String rel) {
        return "<" + uri + ">; rel=\"" + rel + "\"";
    }
}

5. Discoverability at the Root

The root is the entry point in the entire service – it is what the client comes into contact with when consuming the API for the first time. If the HATEOAS constraint is to be considered and implemented throughout, then this is the place to start. The fact that all the main URIs of the system have to be discoverable from the root shouldn’t come as much of a surprise by this point.

Let’s now look at the controller for this:

@RequestMapping( value = "admin",method = RequestMethod.GET )
@ResponseStatus( value = HttpStatus.NO_CONTENT )
public void adminRoot( HttpServletRequest request, HttpServletResponse response ){
    String rootUri = request.getRequestURL().toString();

    URI fooUri = new UriTemplate( "{rootUri}/{resource}" ).expand( rootUri, "foo" );
    String linkToFoo = LinkUtil.createLinkHeader
      ( fooUri.toASCIIString(), "collection" );
    response.addHeader( "Link", linkToFoo );
}

This is of course an illustration of the concept, focusing on a single, sample URI, for Foo Resources – a real implementation should add, similarly, URIs for all the Resources published to the client.

5.1. Discoverability is not about changing URIs

This can be a controversial point – one the one hand, the purpose of HATOAS is to have the client discover the URIs of the API and not rely on hardcoded values. On the other hand – this is not how the web works: yes, URIs are discovered, but they are also bookmarked.

A subtle but important distinction is evolution of the API – the old URIs should still work, but any client that will discover the API should discover the new URIs – which allows the API to change dynamically, and good clients to work well even when the API changes.

In conclusion – just because all URIs of the RESTful web service should be considered cool URIs (and cool URIs don’t change) – that doesn’t mean that adhering to the HATEOAS constraint isn’t extremely useful when evolving the API.

6. Caveats of Discoverability

As some of the discussions around the previous articles state, the first goal of discoverability is to make minimal or no use of documentation and have the client learn and understand how to use the API via the responses it gets. In fact, this shouldn’t be regarded as such a far fetched ideal – it is how we consume every new web page – without any documentation. So, if the concept is more problematic in the context of REST, then it must be a matter of technical implementation, not of a question of whether or not it’s possible.

That being said, technically, we are still far from the a fully working solution – the specification and framework support are still evolving, and because of that, some compromises may have to be made; these are nevertheless compromises and should be regarded as such.

7. Conclusion

This article covered the implementation of some of the traits of discoverability in the context of a RESTful Service with Spring MVC and touched on the concept of discoverability at the root.

The implementation of all these examples and code snippets can be found in my github project – this is an Eclipse based project, so it should be easy to import and run as it is.

I usually post about REST APIs and HTTP on Google+ - you can follow me there:

>> GET THE EBOOKS <<
Get the eBooks and Learn to Build a Simple App
×
Build a Simple but Working App with Spring

, ,

  • http://twitter.com/_mark_burns Mark Burns

    I find this part interesting and I’d like to understand it more:

    “have the client learn and understand how to use the API via the responses it gets. In fact, this shouldn’t be regarded as such a far fetched ideal – it is how we consume every new web page – without any documentation”

    This works well for humans as we can parse and learn and understand the resources. I don’t feel that it’s easy to conceptualise writing flexible clients that can just consume APIs without prior encoded knowledge of some form or another.

    I’m hoping for an interesting rebuttal, but I’ve jotted down some of my thoughts and quite hope that I can be put in my place by someone more knowledgable who can help me see the light.

    If you’re interested take a look here:
    http://vim-and-ruby.tumblr.com/post/12752897221/hateoas

    • Eugen

      Your comment continues the discussion started around the previous post in the series, also about discoverability and HATEOAS. I think there is this idea floating around that if the API would satisfy the HATEOAS constraint and be fully discoverable, then it may be consumable by some kind of non-human client. The way I see it, that is fully outside of what HATEOAS is trying to achieve, mainly to get rid of externally specifying how the client should consume the API and having that interaction driven by the semantically rich responses themselves. That’s not to say there isn’t value in thinking about how a non-human client would better interact with the API, but that’s outside of the current discussion.
      In relation to prior knowledge before first consuming an API, I would see HATEOAS as simply acting as the driver – you give an example of the twitter API – would that be hard to discover? A GET on the root would point you to the URI to GET your tweets, to the URI to POST a new tweet, to the URI to GET your followers, etc. The rich semantics would tell you how to interact with these URIs (which HTTP verbs are supported for instance). Notice how I haven’t made any reference to making the API more flexible by satisfying the HATEOAS constraint.
      These are a few notes on your comment and post, I will revisit and update the comment once I have a chance to read it in more detail. Thanks for the interesting feedback.
      Eugen.

      • Jedediah Smith

        What HATEOAS is trying to achieve is to allow the service to evolve independently from the client. That means the client, on any given visit, must expect just about anything from the server — at least anything that can be expressed in whatever hypermedium is being used which, if it is truly worthy of the “hyper” prefix, should be quite a few different things. To deal with such a variety of responses, the client needs to either be intelligent itself, or limited to generically presenting hypermedia to an intelligent user. This seems to preclude anything we would call an API, which is an interface for programs, not for people.

  • Pingback: Distributed Weekly 173 — Scott Banwart's Blog()