Spring Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

REST Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

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

2. Decoupling Discoverability Through Events

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

First, let’s create 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:

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

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Autowired
    private IFooService service;

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

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

    @PostMapping
    @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));
    }
}

We can then handle these events with any number of decoupled listeners. Each of these can focus on its own particular case and help 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. Making 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.

We’ll handle this using 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());
    }
}

In this example, we’re making use of the ServletUriComponentsBuilder – which helps 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.

4. Getting a Single Resource

On retrieving a single Resource, the client should be able to discover the URI to get all Resources of that 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 headers for the purposes of discoverability. The utility to create this header is simple enough:

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

5. Discoverability at the Root

The root is the entry point in the entire service – it’s 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. Therefore all the main URIs of the system have to be discoverable from the root.

Let’s now look at the controller for this:

@GetMapping("/")
@ResponseStatus(value = HttpStatus.NO_CONTENT)
public void adminRoot(final HttpServletRequest request, final HttpServletResponse response) {
    String rootUri = request.getRequestURL().toString();

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

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 – on the one hand, the purpose of HATEOAS 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 the 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’s 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 a fully working solution – the specification and framework support are still evolving, and because of that, we have to make some compromises.

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 a Maven-based project, so it should be easy to import and run as it is.

Spring bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

REST bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

newest oldest most voted
Notify of
Jedediah Smith
Guest
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,… Read more »