Spring REST Docs
Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test.
Source documentation http://docs.spring.io/spring-restdocs/docs/1.0.x/reference/html5
How RESTful is your API
REST Maturity Model http://martinfowler.com/articles/richardsonMaturityModel.html
Comparison with Swagger
-
Swagger doesn’t support hypermedia (Level 3 maturity model)
-
Swagger documentation is done through several annotations which are added in your implementation classes
-
API Implementation code overtime, gets overwhelmed with swagger annotations
-
Documentation is URI centric
-
Documentation and API could be out-of-sync overtime as there are no validations in place to verify correctness of API
Spring REST Docs Advantages
-
API descriptions and explanation are done in separate .adoc files
-
Ability to structure your documentation based on domain model rather than by URI’s
-
asciidoctor code snippets are generated using Spring Mvc Test, which in turn are plugged into adoc files
-
Guarantees that API documentation and implementation code are always in-sync.
-
Any updates to request, response payloads will fail Unit Tests, until documentation is updated to reflect the changes. Forces you to update documentation for any implementation changes.
-
Ability to document with different payloads and explain different test scenarios
Spring REST Docs Implementation
Add Dependency JARs
Add the following code to the build script:
testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:1.1.0.BUILD-SNAPSHOT"
Spring REST Docs configuration
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); (1)
private RestDocumentationResultHandler document; (2)
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
@Before
public void setUp() {
this.document = document("{method-name}/",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint())); (3)
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration(this.restDocumentation)) (4)
.alwaysDo(this.document)
.build();
}
@Test
public void getAccountsSimpleDoc() throws Exception {
this.mockMvc.perform(get("/accounts"))
.andExpect(status().isOk())
.andDo(this.document); (5)
}
1 | Hook for linking JUnit and Spring REST Docs for generating snippets |
2 | REST Docs snippet Result Handler for formatting result to tables |
3 | Configuration for RestDocumentationResultHandler |
4 | Hook for linking Spring Mvc Test to Spring REST Docs |
5 | Generates asciidoc snippets for curl-request, http-request, http-response, response-fields adoc files |
asciidoctor Configuration
buildscript {
repositories {
mavenCentral()
jcenter() (1)
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE")
classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.3' (2)
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
apply plugin: 'org.asciidoctor.convert' (3)
repositories {
mavenLocal()
maven { url 'https://repo.spring.io/libs-snapshot' }
mavenCentral()
}
1 | Repository for asciidoctor plugin |
2 | Add asciidoctor plugin to classpath |
3 | Apply asciidoctor plugin |
Integrate Spring REST Docs with asciidoc
ext { (1)
snippetsDir = file('build/generated-snippets')
}
test { (2)
outputs.dir snippetsDir
}
asciidoctor { (3)
sourceDir '../../../guides/src/kata-spring-restdocs'
attributes 'snippets': snippetsDir
inputs.dir snippetsDir
dependsOn test
}
1 | Define where snippets would be generated |
2 | Teach test task about snippets directory for gradle to know that when tests are run, snippets are generated in this directory |
3 | Tell asciidoctor about the snippets directory for generating live documentation in the process of gradle continuous builds |
Document APIs using Spring REST Docs
@Test
public void getIndexExample() throws Exception {
this.document.snippets(
links( (1)
linkWithRel("accounts").description("The <<resources-accounts,Account Resource>>"),
linkWithRel("contacts").description("The <<resources-contacts,Contact Resource>>")),
responseFields( (2)
fieldWithPath("_links").description("<<resources-index-links,Links>> to other resources")));
this.mockMvc.perform(get("/"))
.andExpect(status().isOk());
}
@Test
public void getAccountsExample() throws Exception {
this.document.snippets(
responseFields(
fieldWithPath("_links.self").description("Resource Self Link"),
fieldWithPath("_embedded.accountList").description("An array of <<resources-account,Account resources>>")));
this.mockMvc.perform(get("/accounts"))
.andExpect(status().isOk());
}
@Test
public void getAccountExample() throws Exception {
this.document.snippets(
responseFields(
fieldWithPath("id").description("Account unique identifier"),
fieldWithPath("name").description("Account name"),
fieldWithPath("_links.self").description("Account Resource Self Link"),
fieldWithPath("_links.account-contacts").description("Contacts associated for given Account")));
this.mockMvc.perform(get("/accounts/1"))
.andExpect(status().isOk());
// .andExpect(jsonPath("id", is(notNullValue())))
// .andExpect(jsonPath("name", is(notNullValue())))
// .andExpect(jsonPath("_links.self", is(notNullValue())))
// .andExpect(jsonPath("_links.account-contacts", is(notNullValue())));
}
1 | Documenting Link Relationship |
2 | Documenting Response fields |
Generate documentation
Running test target on the project generates the documentation:
$ gradlew test
The results of the run will generate documentation that can now be integrated and presented in a cohesive way. It may look like the one below …
Overview
A sample kata service with resources - Accounts and Contact.
Accounts can have many Contacts. A given contact can belong to many Accounts
Index
The index provides the entry point into the service.
Accessing the index
A GET
request is used to access the index
Example request
$ curl 'http://localhost:8080/' -i
Response structure
Path | Type | Description |
---|---|---|
_links |
Object |
Links to other resources |
Example response
HTTP/1.1 200 OK
Content-Type: application/hal+json
Content-Length: 178
{
"_links" : {
"accounts" : {
"href" : "http://localhost:8080/accounts"
},
"contacts" : {
"href" : "http://localhost:8080/contacts"
}
}
}
Links
Relation | Description |
---|---|
accounts |
The Account Resource |
contacts |
The Contact Resource |
Account
The Account resources
Listing Accounts
A GET
request will list all of the service’s accounts.
Response structure
Path | Type | Description |
---|---|---|
_links.self |
Object |
Resource Self Link |
_embedded.accountList |
Array |
An array of Account resources |
Example request
$ curl 'http://localhost:8080/accounts' -i
Example response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 967
{
"_embedded" : {
"accountList" : [ {
"id" : 1,
"name" : "John",
"_links" : {
"self" : {
"href" : "http://localhost:8080/accounts/1"
},
"account-contacts" : {
"href" : "http://localhost:8080/accounts/1/contacts"
}
}
}, {
"id" : 2,
"name" : "Tim",
"_links" : {
"self" : {
"href" : "http://localhost:8080/accounts/2"
},
"account-contacts" : {
"href" : "http://localhost:8080/accounts/2/contacts"
}
}
}, {
"id" : 3,
"name" : "Mike",
"_links" : {
"self" : {
"href" : "http://localhost:8080/accounts/3"
},
"account-contacts" : {
"href" : "http://localhost:8080/accounts/3/contacts"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/accounts"
}
}
}
Retrieve a Account
A GET
request will retrieve the details of a Account
Response structure
Path | Type | Description |
---|---|---|
id |
Number |
Account unique identifier |
name |
String |
Account name |
_links.self |
Object |
Account Resource Self Link |
_links.account-contacts |
Object |
Contacts associated for given Account |
Example request
$ curl 'http://localhost:8080/accounts/1' -i
Example response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 228
{
"id" : 1,
"name" : "John",
"_links" : {
"self" : {
"href" : "http://localhost:8080/accounts/1"
},
"account-contacts" : {
"href" : "http://localhost:8080/accounts/1/contacts"
}
}
}
Contact
The Contact resource is used to provide contact information
Retrieve a Contact
A GET
request will retrieve the details of a contact
Response structure
Path | Type | Description |
---|---|---|
_embedded.contactList |
Array |
An array of Contact resources |
Example request
$ curl 'http://localhost:8080/accounts/1/contacts' -i
Example response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 619
{
"_embedded" : {
"contactList" : [ {
"id" : 1,
"address" : "Waltham, MA",
"_links" : {
"self" : {
"href" : "http://localhost:8080/contacts/1"
},
"contact-accounts" : {
"href" : "http://localhost:8080/contacts/1/accounts"
}
}
}, {
"id" : 2,
"address" : "Boston, MA",
"_links" : {
"self" : {
"href" : "http://localhost:8080/contacts/2"
},
"contact-accounts" : {
"href" : "http://localhost:8080/contacts/2/accounts"
}
}
} ]
}
}
References
-
Spring REST Docs http://docs.spring.io/spring-restdocs/docs/1.0.x/reference/html5
-
REST Maturity Model http://martinfowler.com/articles/richardsonMaturityModel.html
-
Spring REST Docs Talk at SpringOne2GX 2015 https://youtu.be/k5ncCJBarRI