Sooner or later, most of the developers who actively work on REST(ful) web services and APIs stumble upon this truly extraterrestrial thing called HATEOAS: Hypertext As The Engine Of Application State. The curiosity of what HATEOAS is and how it relates to REST would eventually lead to discovery of the Richardson Maturity Model which demystifies the industry definitions of REST and RESTful. The latter comes as an enlightenment, raising the question however: have we been doing REST wrong all these years?
Let us try to answer this question from the different perspectives. The HATEOAS is one of the core REST architectural constraints. From this perspective, the answer is "yes", in order to claim REST compliance, the web service or API should support that. Nonetheless, if you look around (or even consult your past or present experience), you may find out that the majority of the web services and APIs are just CRUD wrappers around the domain models, with no HATEOAS support whatsoever. Why is that? Probably, there is more than one reason, but from the developer's toolbox perspective, the backing of HATEOAS is not that great.
In today's post we are going to talk about what JAX-RS 2.x has to offer with respect to HATEOAS, how to use that from the server and client perspectives, and how to augment the OpenAPI v3.0.x specification to expose hypermedia as part of the contract. If you are excited, let us get started.
So our JAX-RS web APIs are going to be built around managing companies and their staff. The foundation is Spring Boot and Apache CXF, with Swagger as OpenAPI specification implementation. The AppConfig is the only piece of configuration we need to define in order to get the application up and running (thanks to Spring Boot auto-configuration capabilities).
@SpringBootConfiguration public class AppConfig { @Bean OpenApiFeature createOpenApiFeature() { final OpenApiFeature openApiFeature = new OpenApiFeature(); openApiFeature.setSwaggerUiConfig(new SwaggerUiConfig().url("/api/openapi.json")); return openApiFeature; } @Bean JacksonJsonProvider jacksonJsonProvider() { return new JacksonJsonProvider(); } }
The model is very simple, Company and Person (please notice that there is no direct relationships between these two classes, purposely).
public class Company { private String id; private String name; } public class Person { private String id; private String email; private String firstName; private String lastName; }
This model is exposed through CompanyResource, a typical JAX-RS resource class annotated with @Path, and additionally with OpenAPI's @Tag annotation.
@Component @Path( "/companies" ) @Tag(name = "companies") public class CompanyResource { @Autowired private CompanyService service; }
Great, the resource class has no endpoints defined yet, so let us beef it up. Our first endpoint would lookup the company by identifier and return its representation in JSON format. But since we do not incorporate any staff-related details, it would be awesome to hint the consumer (web UI or any other client) where to look it up. There are multiple ways to do that but since we stick to JAX-RS, we could use Web Linking (RFC-5988) which is supported out of the box. The code snippet is worth thousand words.
@Produces(MediaType.APPLICATION_JSON) @GET @Path("{id}") public Response getCompanyById(@Context UriInfo uriInfo, @PathParam("id") String id) { return service .findCompanyById(id) .map(company -> Response .ok(company) .links( Link.fromUriBuilder(uriInfo .getRequestUriBuilder()) .rel("self") .build(), Link.fromUriBuilder(uriInfo .getBaseUriBuilder() .path(CompanyResource.class)) .rel("collection") .build(), Link.fromUriBuilder(uriInfo .getBaseUriBuilder() .path(CompanyResource.class) .path(CompanyResource.class, "getStaff")) .rel("staff") .build(id) ) .build()) .orElseThrow(() -> new NotFoundException("The company with id '" + id + "' does not exists")); }
There are few things happening here. The one we care about is usage of the ResponseBuilder::links method where we supply three links. The first is self, which is essentially the link context (defined as part of RFC-5988). The second one, collection, is pointing out to the CompanyResource endpoint which returns the list of companies (also is included into standard relations registry). And lastly, the third one is our own staff relation which we assemble from another CompanyResource endpoint implemented by the method with the name getStaff (we are going to see it shortly). These links are going to be delivered in the Link response header and guide the client where to go next. Let us see it in action by running the application.
$ mvn clean package $ java -jar target/jax-rs-2.1-hateaos-0.0.1-SNAPSHOT.jar
And inspect the response from this resource endpoint using curl (the unnecessary details have been filtered out).
$ curl -v http://localhost:8080/api/companies/1 > GET /api/companies/1 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.47.1 > Accept: */* > < HTTP/1.1 200 < Link: <http://localhost:8080/api/companies/1>;rel="self" < Link: <http://localhost:8080/api/companies/1/staff>;rel="staff" < Link: <http://localhost:8080/api/companies>;rel="collection" < Content-Type: application/json < Transfer-Encoding: chunked < { "id":"1", "name":"HATEOAS, Inc." }
The Link header is there, referring to other endpoints of interest. From the client perspective, the things are looking pretty straightforward as well. The Response class provides dedicated getLinks method to wrap around the access to Link response header, for example:
final Client client = ClientBuilder.newClient(); try (final Response response = client .target("http://localhost:8080/api/companies/{id}") .resolveTemplate("id", "1") .request() .accept(MediaType.APPLICATION_JSON) .get()) { final Optional staff = response .getLinks() .stream() .filter(link -> Objects.equals(link.getRel(), "staff")) .findFirst(); staff.ifPresent(link -> { // follow the link here }); } finally { client.close(); }
So far so good. Moving forward, since HATEOAS is essentially a part of the web APIs contract, let us find out what OpenAPI specification has for it on the table. Unfortunately, HATEOAS is not supported as of now, but on the bright side, there is a notion of links (although they should not be confused with Web Linking, they are somewhat similar but not the same). To illustrate the usage of the links as part of the OpenAPI specification, let us decorate the endpoint with Swagger annotations.
@Operation( description = "Find Company by Id", responses = { @ApiResponse( content = @Content(schema = @Schema(implementation = Company.class)), links = { @io.swagger.v3.oas.annotations.links.Link( name = "self", operationRef = "#/paths/~1companies~1{id}/get", description = "Find Company", parameters = @LinkParameter(name = "id", expression = "$response.body#/id") ), @io.swagger.v3.oas.annotations.links.Link( name = "staff", operationRef = "#/paths/~1companies~1{id}~1staff/get", description = "Get Company Staff", parameters = @LinkParameter(name = "id", expression = "$response.body#/id") ), @io.swagger.v3.oas.annotations.links.Link( name = "collection", operationRef = "#/paths/~1companies/get", description = "List Companies" ) }, description = "Company details", responseCode = "200" ), @ApiResponse( description = "Company does not exist", responseCode = "404" ) } ) @Produces(MediaType.APPLICATION_JSON) @GET @Path("{id}") public Response getCompanyById(@Context UriInfo uriInfo, @PathParam("id") String id) { // ... }
If we run the application and navigate to the http://localhost:8080/api/api-docs in the browser (this is where Swagger UI is hosted), we would be able to see the links section along each response.
But besides that ... not much you could do with the links there (please watch for this issue if you are interested in the subject). The resource endpoint to get the company's staff is looking quite similar.
@Operation( description = "Get Company Staff", responses = { @ApiResponse( content = @Content(array = @ArraySchema(schema = @Schema(implementation = Person.class))), links = { @io.swagger.v3.oas.annotations.links.Link( name = "self", operationRef = "#/paths/~1companies~1{id}~1staff/get", description = "Staff", parameters = @LinkParameter(name = "id", expression = "$response.body#/id") ), @io.swagger.v3.oas.annotations.links.Link( name = "company", operationRef = "#/paths/~1companies~1{id}/get", description = "Company", parameters = @LinkParameter(name = "id", expression = "$response.body#/id") ) }, description = "The Staff of the Company", responseCode = "200" ), @ApiResponse( description = "Company does not exist", responseCode = "404" ) } ) @Produces(MediaType.APPLICATION_JSON) @GET @Path("{id}/staff") public Response getStaff(@Context UriInfo uriInfo, @PathParam("id") String id) { return service .findCompanyById(id) .map(c -> service.getStaff(c)) .map(staff -> Response .ok(staff) .links( Link.fromUriBuilder(uriInfo .getRequestUriBuilder()) .rel("self") .build(), Link.fromUriBuilder(uriInfo .getBaseUriBuilder() .path(CompanyResource.class) .path(id)) .rel("company") .build() ) .build()) .orElseThrow(() -> new NotFoundException("The company with id '" + id + "' does not exists")); }
As you might expect, beside the link to self, it also includes the link to the company. When we try it out using curl, the expected response headers are returned back.
$ curl -v http://localhost:8080/api/companies/1/staff > GET /api/companies/1/staff HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.47.1 > Accept: */* > < HTTP/1.1 200 < Link: <http://localhost:8080/api/companies/1/staff>;rel="self" < Link: <http://localhost:8080/api/companies/1>;rel="company" < Content-Type: application/json < Transfer-Encoding: chunked < [ { "id":"1", "email":"john@smith.com", "firstName":"John", "lastName":"Smith" }, { "id":"2", "email":"bob@smith.com", "firstName":"Bob", "lastName":"Smith" } ]
So what kind of conclusions we can draw from that? HATEOAS indeed unifies the interaction model between web API providers and consumers by dynamically driving the conversations. This is very powerful, but most of the frameworks and tools out there either have pretty basic support of the HATEOAS (for example, Web Linking) or none at all.
There are many use cases when usage of the Web Linking is sufficient (the examples we have seen so far, paging, navigation, ...), but what about let say creating, editing or patching the existing resources? What about enriching with hypermedia the individual elements which are returned in the collection (described in RFC-6537)? Is HATEOAS worth all this efforts?
As always, the answer is "it depends", may be we should look beyond the JAX-RS? In the next post(s_ we are going to continue figuring things out.
The complete source code is available on Github.