by Sebastian Daschner
Implementing a fully-fledged hypermedia-driven REST API in your Java enterprise application enables more flexibility and resilience towards changes. As a positive side effect, the need to document every single REST resource is eliminated as well.
Everybody is doing REST—at least everybody claims they are. But most web APIs you can find out there are something between "RPC style" and "basic resources with HTTP semantics"—except they are built without hypermedia.
Real-World "REST" Examples
Many real-world projects are mistakenly calling any kind of HTTP API "RESTful." Consider the example shown in Listing 1.
POST /doSomeAction
<someActionRequest>
<param>12345</param>
</someActionRequest>
200 OK
<someActionResponse>
<value>2345</value>
</someActionResponse>
Listing 1. RPC HTTP example (request and response)
These HTTP calls in fact describe method calls over HTTP. The URL doSomeAction
describes a method name, POST is used for all calls (even to read data), and the request and response bodies describe input and output parameters.
You might conclude that this is nothing other than SOAP without the SOAP envelope.
Resources and HTTP Semantics
The URLs of a REST API represent the business entities of the application. This means you need to think in terms of objects rather than verbs when designing the API.
For instance, for a user management module, the user objects are exposed via HTTP, as shown in Listing 2.
GET /users
200 OK
<users>
<user>
<id>12345</id>
<name>Duke</name>
<motto>Java rocks!</motto>
</user>
<!-- some more users ... -->
</users>
Listing 2. Resources example (request and response)
The differences here are that the URLs reflect the business objects (the list of all users) and the correct HTTP method is used to read a resource.
If the client wants to read all users and then follow each user's resource, it would need to know the logic for how to construct the URLs. Assuming the URL of a specific user is /users/12345
, the client needs to implicitly concatenate the URL of all users with the ID of the corresponding object. This tightly couples the client to the logic, which should reside only on the server.
A common misconception is that REST is somewhat about predictable URLs. In fact, readable URLs are nice to have, but more important is the fact that the server alone stays in control of the URLs and actively guides the client to the needed resources.
How can you achieve that?
Listing 3 shows another example of how a user is created using resources and HTTP semantics.
POST /users
<user>
<name>Duke</name>
<motto>JAX-RS rocks!</motto>
</user>
201 Created
Location: /users/12345
Listing 3. Creating resources (request and response)
The server would respond to the request with HTTP status 201 Created
and a Location
header field containing the URL of the newly created resource. The client uses this URL for later access to the specific user's resource. This no longer implies that the URL is to be constructed on the client side, and the server is in control of its URLs again.
Enter Hypermedia
Directing the client regarding where to access related resources is the point at which hypermedia steps in.
Consider the example of the list of users again, but this time with resource links, as shown in Listing 4.
GET /users
200 OK
<users>
<user>
<name>Duke</name>
<motto>Java rocks!</motto>
<link rel="self" href="/users/12345"/>
</user>
<!-- some more users ... -->
</users>
Listing 4. Resources with links example (request and response)
The response no longer contains the IDs of the business objects rather than links to the corresponding resources. The client can follow these links without prior knowledge of the application's structure. The only knowledge needed is the self
relation. This tells the client that the link is the resource of the containing object (the user) itself.
As a consequence, the links are taken to direct the client to all needed locations, only with the knowledge of the relations. The rest is up to the server.
Listing 5 shows this resource link approach with another aspect—this time with a book store example in JSON format.
GET /books/12345
200 OK
{
"name": "Java",
"author": "Duke",
// + availability, price
"_links": {
"self": "/books/12345",
"add-to-cart": "/shopping_cart"
}
}
Listing 5. Resource with several links (request and response)
The resource not only contains the link of the self relation but also a URL where the "add to shopping cart" functionality can be accessed.
This implies another benefit of using hypermedia. That link would be included only if the user is allowed to add the book to the cart. Consider the book store example where books can be bought, but the books can be added to the user's shopping cart only if several preconditions match. This business logic (for example, add only books that are in stock) ideally resides only on the server to prevent redundant logic.
Instead of duplicating the logic (show the shopping cart button only if the book is in stock) to the client, the server includes the add-to-cart
link in the response body only if that logic applies. The client knows the add-to-cart
relation and will display a fancy button and access that URL if the button is clicked.
But how does the API user know which data must be sent to the add-to-cart
URL? The action would not be made via an HTTP GET
call rather than a POST
with needed data, for example, the book ID, how many books should be added, and so on. This information must either be documented (that is, a description of how to access that URL) or included in the API via a more sophisticated hypermedia approach.
There are several hypermedia-aware content types that allow different levels of control. See M. Amundsen's entry on H Factor.
One hypermedia-aware content type is Siren. It not only allows you to define links but also so-called actions, describing how resources are accessed in ways other than by using simple GET
calls.
Listing 6 shows an example of a book with links and actions.
GET /books/12345
200 OK
{
"class": [ "book" ],
"properties": {
"isbn": "1-1234-5678",
"name": "Java",
"author": "Duke",
"availability": "IN_STOCK",
"price": 29.99
},
"actions": [
{
"name": "add-to-cart",
"title": "Add Book to cart",
"method": "POST",
"href": "http://api.jamazon.example.com/shopping_cart",
"type": "application/json",
"fields": [
{ "name": "isbn", "type": "text" },
{ "name": "quantity", "type": "number" }
]
}
],
"links": [
{ "rel": [ "self" ], "href": "http://api.jamazon.example.com/books/1234" }
]
}
Listing 6. Siren book resource example (request and response)
The client receives all the needed information to make the call for the add-to-cart
functionality, including the URL, the HTTP method, the content type, and all required content data in the form of JSON properties. The only knowledge left is where the information for the isbn
and quantity
fields come from. This is business logic, which obviously must reside on the client.
As a result, the client can navigate through the API in a self-exploratory fashion without any prior knowledge besides the domain model of the application. A lot of documentation can be saved here, because the "how to use" information is baked into the API itself.
Besides that, using hypermedia also enables a higher flexibility for API changes, which the client is able to adapt to.
In general, the bigger your API and the greater the variety of its consuming clients, the more it makes sense to make the REST API hypermedia driven.
Enter Java EE
JAX-RS is the standard for developing RESTful web services under the Java EE umbrella.
Listings 7 and 8 show how a JAX-RS resource class that handles HTTP calls (shown in Listing 5) and an annotated plain old Java object (POJO) might look.
@Path("books")
@Produces(MediaType.APPLICATION_JSON)
public class BooksResource {
@Inject
BookStore bookStore;
@Context
UriInfo uriInfo;
@GET
public List<Book> getBooks() {
final List<Book> books = bookStore.getBooks();
books.stream().forEach(u -> {
final URI selfUri = uriInfo.getBaseUriBuilder().path(BooksResource.class).path(BooksResource.class, "getBook").build(u.getId());
u.getLinks().put("self", selfUri);
});
return books;
}
@GET
@Path("{id}")
public Book getBook(@PathParam("id") final long id) {
final Book book = bookStore.getBook(id);
final URI selfUri = uriInfo.getBaseUriBuilder().path(BooksResource.class).path(BooksResource.class, "getBook").build(book.getId());
book.getLinks().put("self", selfUri);
// check if business logic applies
if (bookStore.isAddToCartAllowed(book)) {
final URI addToCartUri = uriInfo.getBaseUriBuilder().path(CartResource.class).build();
book.getLinks().put("add-to-cart", addToCartUri);
}
return book;
}
}
Listing 7. JAX-RS resource class
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Book {
@XmlTransient
private long id;
private String name;
private String author;
@XmlElement(name = "_links")
private Map<String, URI> links = new HashMap<>();
// getters & setters
}
Listing 8. Book example POJO
The getBooks
resource handler calls the BookStore
managed bean to retrieve the books, which are enriched with the corresponding URIs and returned to the client. Note that even for the application/json
content type, Java Architecture for XML Binding (JAXB) annotations can be used to control the mapping of Java objects to JSON output. The major JSON mapping frameworks shipped with application servers take the JAXB annotations into account as well.
The UriInfo
functionality of JAX-RS is used to programmatically create the URIs with information derived directly from the JAX-RS classes. The path method concatenates the path pieces from the @Path
annotations. The getBook
method contains a path parameter that needs to be substituted with the actual ID of the book. This is done by calling the build
method with a corresponding number of arguments.
Another helpful feature of the UriInfo
component is the getBaseUriBuilder
builder, which constructs the protocol, host, and ports depending on the given HTTP request. If the application server is proxied, for example, with Apache or NGINX, then the originally requested protocol, host, and port of the HTTP request and—therefore, the base URL of the proxy server—is taken to construct the URIs. This comes out of the box with JAX-RS and minimizes the amount of configuration and tight coupling of the Java EE application with its IT landscape.
The getBook
resource handler does basically the same thing only for one book, and it also includes the add-to-cart
link if the business logic matches. This is an example of how hypermedia is used to control the client's behavior.
Listings 9 and 10 show potential responses of the given example.
GET /hypermedia-test/resources/books
200 OK
[
{ "name": "Java",
"author": "Duke",
"_links": {
"self": "http://localhost:8080/hypermedia-test/resources/books/1",
}
},
{
"name": "Hello",
"author": "World",
"_links": {
"self": "http://localhost:8080/hypermedia-test/resources/books/2"
}
}
]
Listing 9. Books resource example
GET /hypermedia-test/resources/books/1
200 OK
{
"name": "Java",
"author": "Duke",
"_links": {
"self": "http://localhost:8080/hypermedia-test/resources/books/1"
"add-to-cart": "http://localhost:8080/hypermedia-test/resources/shopping_cart"
}
}
Listing 10. Book resource example
The more-sophisticated example using a content type, such as Siren, needs more information in the response. There are libraries, such as Siren4J, which provide functionality to create responses in the desired hypermedia content type.
However, I'll show an approach using plain Java EE 7 without any external library to avoid additional dependencies and to keep the resulting deployment artifacts as small as possible. This means you either create your own Java classes containing fields according to the content type or you use a programmatic approach. JSONP offers functionality to programmatically create JSON structures with maximum control.
Listing 11 shows how to programmatically create a book resource response using JSONP.
@GET
public JsonArray getBooks() {
return bookStore.getBooks().stream().map(b -> {
final URI selfUri = uriInfo.getBaseUriBuilder().path(BooksResource.class).path(BooksResource.class, "getBook").build(b.getId());
final JsonObject linksJson = Json.createObjectBuilder().add("self", selfUri.toString()).build();
return Json.createObjectBuilder()
.add("name", b.getName())
.add("author", b.getAuthor())
.add("_links", linksJson).build();
}).collect(Json::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::add).build();
}
@GET
@Path("{id}")
public JsonObject getBook(@PathParam("id") final long id) {
final Book book = bookStore.getBook(id);
final URI selfUri = uriInfo.getBaseUriBuilder().path(BooksResource.class).path(BooksResource.class, "getBook").build(book.getId());
final JsonObjectBuilder linksJson = Json.createObjectBuilder().add("self", selfUri.toString());
if (bookStore.isAddToCartAllowed(book)) {
final URI addToCartUri = uriInfo.getBaseUriBuilder().path(CartResource.class).build();
linksJson.add("add-to-cart", addToCartUri.toString());
} return Json.createObjectBuilder()
.add("name", book.getName())
.add("author", book.getAuthor())
.add("_links", linksJson).build();
}
Listing 11. Book resource example using JSONP
I'm using Java 8 language features such as streams, lambda expressions, and method handles to programmatically construct JsonObject
s and JsonArray
s of the retrieved POJOs. With this approach, the developer is in full control of how the response can look and what content type is used.
In real-world projects, this functionality for how the business objects are mapped to a hypermedia response containing links and actions would be separated into a single component, such as a Contexts and Dependency Injection (CDI) managed bean, and reused in all REST resource classes within the project.
More-complete examples can be found on my JAX-RS Hypermedia GitHub project. The examples include how to create hypermedia APIs with resource links only and how to include actions (realized via Siren) using different approaches.
Conclusion
That's all you need in a Java EE 7 application to create a hypermedia-driven REST API.
The most important design choices upfront are which level of hypermedia it makes sense to apply, which HTTP content type to take, and whether it's reasonable to use a third-party dependency to create responses versus going with a "plain Java EE 7" approach.
See Also
About the Author
Sebastian Daschner is a Java freelancer working as a consultant, software developer, and architect. He is enthusiastic about programming and Java EE and is participating in the JCP, serving in the JSR 370 Expert Group and hacking on various open source projects on GitHub. He has been working with Java for more than six years. Besides Java, Daschner is also a heavy user of Linux and container technologies such as Docker. He evangelizes computer science practices on his blog and on Twitter.
Join the Conversation
Join the Java community conversation on Facebook, Twitter, and the Oracle Java Blog!