API Documentation with Spring REST Docs
Context
Most of backend developer's daily work is based on the use of APIs. As new technologies, patterns and architectures develop, it is more important to learn how to improve our APIs so they can be easily used and understood by others. This fact is more present when working in microservices oriented architectures, where many APIs interact, both for internal and external communication. Usually, these APIs are implemented by different teams which makes the documentation of these APIs really important. There are several ways to document an API: from the deprecated Word document where you can never be sure if the information is up-to-date, to more modern tools such as Swagger, where you can document your API using annotations in your code, which ensures the documentation is a reflection of the code, but makes your code a little bit more difficult to read.
Luckily for us who work with Spring Framework to implement APIs, there is Spring REST Docs, which makes the generation and, most importantly, the maintenance of API documentation very simple.
Spring REST Docs ensures your API documentation always reflects what your API does, as long as you have a good coverage of your controllers with unit tests. Spring REST Docs uses the data you provide for your tests to generate snippets for your documentation. How cool is that, uh? This looks like a very cool solution, right? Let's see an example of how it works.
Hands on!
Generating snippets
First of all, we need to configure the required dependencies and plugins to include Spring Rest Docs in our project. You can follow the instructions defined in the documentation to configure your project either with Maven or Gradle. As you can see in the documentation, the unit tests can be written using Spring MVC Test support, Spring Webflux's WebTestClient or REST Assured 3. Gradle and Spring MVC Test are used for this example.
Once our project is properly configured, we will start with the an API which defines the following method to create new Customers:
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Customer create(@RequestBody @Valid Customer customer) {
Assert.isNull(customer.getId(), "Id must be null to create a new customer");
return this.customersRepository.save(customer);
}
Now, let's go to the unit tests. First, we need to configure the MockMvc
object so the default snippets are generated for each test in the class.
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
// …
.alwaysDo(document("{method-name}",
preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
.build();
}
Next, we see the unit test covering the execution of the create endpoint:
@Test
public void createCustomer() throws Exception {
Customer customer = new Customer();
customer.setAge(23);
customer.setFirstName("Liam");
customer.setGender(Gender.MALE);
customer.setPhoneNumber("1119992");
Customer savedCustomer = new Customer(customer);
savedCustomer.setId(34L);
when(this.customersRepository.save(any(Customer.class))).thenReturn(savedCustomer);
this.mockMvc.perform(post("/customers").content(this.objectMapper.writeValueAsString(customer))
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.firstName").value("Liam"))
.andExpect(jsonPath("$.lastName").isEmpty())
.andExpect(jsonPath("$.gender").value(Gender.MALE.name()))
.andExpect(jsonPath("$.phoneNumber").value("1119992"))
.andExpect(jsonPath("$.age").value(23));
}
When running these tests, we can see that a new folder called generated-snippets is created under the build directory. This new folder contains another folder with the name of the test method and it contains some asciidoc files:
Each of these files contains the generated documentation of the create endpoint from the Customers API, based on the data provided in the unit test.
But we can go beyond the default snippets, we can improve the documentation by adding a new action to the test with andDo(document(...)) . For instance, we can document the payload of the request and the response with the methods requestFields and responseFields, respectively.
@Test
public void createCustomer() throws Exception {
Customer customer = new Customer();
customer.setAge(23);
customer.setFirstName("Liam");
customer.setGender(Gender.MALE);
customer.setPhoneNumber("1119992");
Customer savedCustomer = new Customer(customer);
savedCustomer.setId(34L);
when(this.customersRepository.save(any(Customer.class))).thenReturn(savedCustomer);
FieldDescriptor[] customerDescriptor = getCustomerFieldDescriptor();
this.mockMvc.perform(post("/customers", 330).content(this.objectMapper.writeValueAsString(customer))
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.firstName").value("Liam"))
.andExpect(jsonPath("$.lastName").isEmpty())
.andExpect(jsonPath("$.gender").value(Gender.MALE.name()))
.andExpect(jsonPath("$.phoneNumber").value("1119992"))
.andExpect(jsonPath("$.age").value(23))
.andDo(document("create-customer",
requestFields(customerDescriptor),
responseFields(customerDescriptor)));
}
We can use the method fieldWithPath to document the fields of the request and response, where we can define its description, if it is optional, the data type, etc. If the same object is used in both request and response, we can use a FieldDescriptor to not repeat the same code:
private FieldDescriptor[] getCustomerFieldDescriptor() {
return new FieldDescriptor[]{fieldWithPath("age").description("The age of the customer").type(Integer.class.getSimpleName()),
fieldWithPath("firstName").description("The first name of the customer").type(String.class.getSimpleName()),
fieldWithPath("gender").description("The gender of the customer (FEMALE or MALE)").type(Gender.class.getSimpleName()),
fieldWithPath("phoneNumber").description("The cell phone number of the customer").type(String.class.getSimpleName()),
fieldWithPath("id").description("The unique id of the customer").optional().type(Long.class.getSimpleName()),
fieldWithPath("lastName").description("The last name of the customer").optional().type(String.class.getSimpleName())};
}
If we run the test again, two new snippet files are generated: request-fields.adoc and response-fields.adoc, which contain the description we set up for the request and response bodies, respectively.
Creating documentation
OK, so this seems really easy but, how do I use these snippets as my API documentation? We can create one or multiple asciidoc pages that include the generated snippets and use the asciidoc plugin to convert these pages into HTML content.
For instance, let's create a new page index.adoc in the folder src/docs/asciidoc (as we are using Gradle). This page is very simple, it just contains some introduction text and includes some of the generated snippets. The file index.adoc looks like:
= Getting started with Spring REST Docs
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toc-title: Index
:toclevels: 4
:sectlinks:
This is the documentation of the resource Customers API.
=== POST /customers
.Request sample with curl
include::{snippets}/create-customer/curl-request.adoc[]
.HTTP response
include::{snippets}/create-customer/http-response.adoc[]
.Response sample
include::{snippets}/create-customer/response-body.adoc[]
.Input fields
include::{snippets}/create-customer/request-fields.adoc[]
.Output fields
include::{snippets}/create-customer/response-fields.adoc[]
The first lines starting with : are related to the configuration of the look and feel of the documentation. You can find more detailed information about asciidoc syntax in the asciidoc documentation.
Now when we build the application, in our case by using gradle build
, a new folder asciidoc/html5 will be generated in the build directory with the HTML content, in this case only an index.html file. When we open this file in the browser we will see something like:
Cool, right? Now we are going to change the create endpoint by adding a new and mandatory path variable:
@PostMapping("/{salesRepId}")
@ResponseStatus(HttpStatus.CREATED)
public Customer create(@RequestBody @Valid Customer customer, @PathVariable Long salesRepId) {
Assert.isNull(customer.getId(), "Id must be null to create a new customer");
validate(salesRepId, customer);
return this.customersRepository.save(customer);
}
The test created in the previous step now fails as the API has changed. We just need to adapt it to include the new changes:
@Test
public void createCustomer() throws Exception {
Customer customer = new Customer();
customer.setAge(23);
customer.setFirstName("Liam");
customer.setGender(Gender.MALE);
customer.setPhoneNumber("1119992");
Customer savedCustomer = new Customer(customer);
savedCustomer.setId(34L);
when(this.customersRepository.save(any(Customer.class))).thenReturn(savedCustomer);
FieldDescriptor[] customerDescriptor = getCustomerFieldDescriptor();
this.mockMvc.perform(RestDocumentationRequestBuilders.post("/customers/{salesRepId}", 330).content(this.objectMapper.writeValueAsString(customer))
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.firstName").value("Liam"))
.andExpect(jsonPath("$.lastName").isEmpty())
.andExpect(jsonPath("$.gender").value(Gender.MALE.name()))
.andExpect(jsonPath("$.phoneNumber").value("1119992"))
.andExpect(jsonPath("$.age").value(23))
.andDo(document("create-customer",
requestFields(customerDescriptor),
responseFields(customerDescriptor)));
}
To make the test run we just added two changes: add the variable salesRepId to the path and use the method post from the class RestDocumentationRequestBuilder
so we can document the path variable with Spring REST Docs.
When running the test again, we will see that the snippets have been updated with the new code, but no new snippets have been created. We can add a path-parameter.adoc snippet by documenting the path variable as it is shown below:
@Test
public void createCustomer() throws Exception {
//…
.andDo(document("create-customer",
requestFields(customerDescriptor),
pathParameters(
parameterWithName("salesRepId").description("Unique identifier of the Sales Rep responsible of the customer")
),
responseFields(customerDescriptor)));
}
Now, the snippet path-parameters.adoc is generated and it can be included in the documentation.
Spring REST Docs can also generate other snippets depending on the implementation of each endpoint. You can check all the possibilities in the official documentation.
Conclusion
Spring REST Docs facilitates the maintenance of the API documentation in Spring projects, and guarantees that, as long as your API is covered with unit tests, your documentation will be up to date. If you are working in a project with Spring, what are you waiting for?
You can see the complete project in Github at: https://github.com/mimacom/spring-rest-docs.