The Hypermedia APIs Support in JAX-RS and OpenAPI: a Long Way to Go
Have we been doing REST wrong all these years?
Join the DZone community and get the full member experience.
Join For FreeSooner 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 the discovery of the Richardson Maturity Model, which demystifies the industry definitions of REST and RESTful. The latter comes as enlightenment, raising the question, however: have we been doing REST wrong all these years?
Let us try to answer this question from 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 are 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 look up the company by the 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 below is worth a 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 a few things happening here. The one we care about is the 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 the 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, besides the link toself
, it also includes the link to the company. When we try it out usingcurl
, 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's 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 these efforts?
As always, the answer is "it depends," maybe 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.
Published at DZone with permission of Andriy Redko, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments