REST (Representational State Transfer) outlines a way of designing web services around resources and metadata using HTTP methods like GET, POST, PUT, and PATCH to map to well-defined actions. It was first defined by Roy Fielding in his 2000 PhD dissertation “Architectural Styles and the Design of Network-based Software Architectures” at UC Irvine.1 A web service that adheres to these principles is called RESTful
.
This chapter is primarily about two Spring projects, Spring REST Docs and Spring HATEOAS.2 It builds on the content from Chapter 7, so be sure to read it first before reading this chapter. Although using these projects is not required to build a RESTful web service, using them together with Spring MVC allows you to build a fully featured web API all using Spring.
Spring REST Docs
Spring REST Docs3 generates
documentation based on tests combined with text documents using the Asciidoctor syntax, although you may use Markdown instead. This approach is meant to generate API documents, similar to Swagger, but with more flexibility.
Spring REST Docs uses snippets produced by tests written with Spring MVC’s MockMvc, Spring WebFlux’s WebTestClient, or REST Assured 3.4 This test-driven approach helps to guarantee the accuracy of your web service’s documentation. If a snippet is incorrect, the test that produces it fails.
Getting Started
To get started, first add the Spring REST Docs dependency to your project. If using
Maven, add the following
dependency:
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<version>2.0.4.RELEASE</version>
<scope>test</scope>
</dependency>
Also, add the following
Maven plugin which will process the asciidoctor text during the prepare-package phase:
<build>
<plugins>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.8</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>2.0.4.RELEASE</version>
If using a
Gradle build, use the following build file:
plugins {
id "org.asciidoctor.convert" version "2.4.0"
id "java"
}
ext {
snippetsDir = file('build/generated-snippets')
ver = '2.0.4.RELEASE'
}
dependencies {
asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:$ver"
testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$ver"
}
test {
outputs.dir snippetsDir
}
asciidoctor {
inputs.dir snippetsDir
dependsOn test
}
REST Docs Generation
To generate REST Docs from an existing Spring MVC–based project, you need to write unit or integration tests for each request/response you want to document and include the JUnitRestDocumentation rule on your test.
For example, define a test using
@SpringBootTest, or otherwise set up your application context in the setUp method of your test, and using
@Rule, define an instance of
JUnitRestDocumentation:
@RunWith(SpringRunner.class)
@SpringBootTest
public class GettingStartedDocumentation {
@Rule
public final JUnitRestDocumentation restDocumentation =
new JUnitRestDocumentation();
Then set up the MockMvc instance
@Before
public void setUp() {
this.mockMvc =
MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration(this.restDocumentation))
.alwaysDo(document("{method-name}/{step}/",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint())))
.build();
}
using the following static imports
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static
org.springframework.restdocs.mockmvc.MockMvcRestDocumentation
.documentationConfiguration;
For each test
method within the JUnit test which uses
mockMvc, Spring REST Docs will now create (during the build) a directory named by converting the test’s name from CamelCase to dash-separated names (e.g.,
creatingACourse becomes creating-a-course) and a number-indexed directory for each HTTP request. For example, if there are four requests in a test you will have directories
1/ 2/ 3/ and
4/. Each HTTP request in turn gets the following snippets generated:
curl-request.adoc
httpie-request.adoc
http-request.adoc
http-response.adoc
request-body.adoc
response-body.adoc
Then, you can write the Asciidoctor documentation under the
src/docs/asciidoc/ directory and include the generated snippets into your output, for example:
include::{snippets}/creating-a-course/1/curl-request.adoc[]
This text is included in output.
include::{snippets}/creating-a-course/1/http-response.adoc[]
This
would include each of the preceding snippets within your documentation output (typically HTML5 output).
Serving the Documentation in Spring Boot
To serve the
HTML5 generated documentation in a Spring Boot–based project, add the following to your Gradle build file:
bootJar {
dependsOn asciidoctor
from ("${asciidoctor.outputDir}/html5") {
into 'static/docs'
}
}
Spring HATEOAS
Closely related to REST is the concept of Hypermedia as the engine of application state (HATEOAS),5 which outlines how each response from a web service should provide information, or links, that describe other endpoints, much like how websites work. Spring HATEOAS6 helps enable these types of RESTful web services.
Getting Started
To get started, first add the Spring HATEOAS dependency to your project. If using Spring Boot and Maven, add the following
dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
If using Spring Boot with Gradle, use the following dependency:
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
Creating Links
The key part of HATEOAS is the link, which can contain a URI or URI template and allows the client to easily navigate the REST API and provides for future compatibility – the client can use the link allowing the server to change where that link points.
Spring HATEOAS supplies methods for easily creating Links, such as the LinkBuilder and WebMvcLinkBuilder. It also supplies models for representing Links in the response, such as EntityModel, PagedModel, CollectionModel, and RepresentationModel. Which model to use depends on what type of data you are returning one entity (EntityModel), pages of data (PagedModel), or others.
Let’s take one example using the
WebMvcLinkBuilder and the
EntityModel:package com.apress.spring_quick.rest;
import org.springframework.hateoas.EntityModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
@RestController
public class GettingStartedController {
@GetMapping("/")
public EntityModel<Customer> getCustomer() {
return EntityModel.of(new Customer("John", "Doe"))
.add(linkTo(GettingStartedController.class).withSelfRel())
.add(linkTo(GettingStartedController.class)
.slash("next").withRel("next"));
}
}
At
runtime, this endpoint would return the following as JSON (when running locally):
{
"firstname":"John",
"lastname":"Doe",
"_links":{
"self":{"href":"http://localhost:8080"},
"next":{"href":"http://localhost:8080/next"}
}
}
Testing
Testing the HATEOAS output can be achieved similarly to testing any web service that produces XML or JSON.
In the common case where your service produces JSON, it would be helpful to use a library to navigate the JSON in a similar way to how XPath navigates XML documents, JsonPath. One library that implements JsonPath in Java is Jayway JsonPath.8 Although you can use it directly, Spring wraps the usage of JsonPath with the static MockMvcResultMatchers.jsonPath method for ease of use with Hamcrest matchers.
To use JsonPath, we simply need to include a dependency in the Maven pom:
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.4.0</version>
<scope>test</scope>
</dependency>
Or if using Gradle, include
testCompile 'com.jayway.jsonpath:json-path:2.4.0'
For example, see the following JUnit test class which uses JsonPath to validate that
_links.self and
_links.next are not null:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.*;
@ExtendWith(SpringExtension.class) // JUnit 5
@SpringBootTest
public class GettingStartedDocumentation {
@Autowired
private WebApplicationContext context;
@BeforeEach
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.build();
}
@Test
public void index() throws Exception {
this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("_links.self", is(notNullValue())))
.andExpect(jsonPath("_links.next", is(notNullValue())));
}
}
Listing 11-1GettingStartedDocumentation.java