How to integrate Spring REST Docs with Javadocs
Tags: springdocs, springrestdocs, javadocs, java
Published: 2022-11-16 23:47:25

Learn how to properly and continuously document your Spring REST API with Spring REST Docs.

## 1. Introduction
There are many ways to generate Spring REST docs. One popular way is by using Swagger but in this blog, I will share an alternative through Spring REST Docs.

This tutorial requires a project with the following stack:

- Spring REST
- Maven

## 2. Spring REST Docs
Spring REST Docs generate documentation from an Asciidoctor template and auto-generated snippets produced with Spring MVC tests. Unlike Swagger, it uses JavaDocs for class, method, and parameter definitions including constraints.

One advantage of using this library is that missing a parameter definition (request, path, etc) will break the integration tests. So you can't really proceed and make a release without fixing them first. Thus, it produced always updated, concise, and well-structured documentation.

### 2.1 Spring REST Docs Dependencies

```
<dependency>
  <groupId>org.springframework.restdocs</groupId>
  <artifactId>spring-restdocs-mockmvc</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>capital.scalable</groupId>
  <artifactId>spring-auto-restdocs-core</artifactId>
  <scope>test</scope>
</dependency>
```

## 3. Let's Start Documenting our REST API

### 3.1 Our REST API

```
/**
 * Creates, and maps an SSO user to an internal user table.
 *
 * @param platformUserInboundDto - JSON object
 * @return a CompletableFuture instance of {@link PlatformUserOutboundDto}
 */
@PostMapping(
    path = EndpointConstants.PATH_USERS,
    produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Transactional
public CompletableFuture<PlatformUserOutboundDto> mapUpdateOrCreateIfAbsent(
    @Valid @RequestBody PlatformUserInboundDto platformUserInboundDto) {
 
  log.debug("Check if user with externalRef={} exists", platformUserInboundDto.getExternalRef());
 
  return platformSsoService.createIfNotExists(web2ServiceMapper.toPlatformUser(platformUserInboundDto))
      .thenApply(service2WebMapper::toPlatformUserOutboundDto);
}
```

For this exercise, let's assume we have an endpoint that creates or maps an SSO user if it doesn't exist in the local system. It accepts an object parameter that contains the user information. Above the method is the JavaDoc.

Here's the input DTO which as we can see is annotated with constraints.

```
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PlatformUserInboundDto {
 
  /**
   * Unique id from the SSO provider.
   */
  @NotNull
  @NotEmpty
  @Size(max = 50)
  private String externalRef;
 
  @NotNull
  @NotEmpty
  @Size(max = 255)
  private String email;
 
  @Size(max = 50)
  private String identityProvider;
 
  @Size(max = 255)
  private String firstName;
 
  @Size(max = 255)
  private String lastName;
   
  //..
}
```

### 3.2 Our REST API Test 
We will follow the Spring MockMvc test here https://spring.io/guides/gs/testing-web.

```
// In your test class define and initialize the MockMvc object on every test. Thus, we need to create a method annotated with @BeforeEach
protected MockMvc mockMvc;
 
@BeforeEach
  void setup(RestDocumentationContextProvider restDocumentation) {
    this.mockMvc =
        MockMvcBuilders.webAppContextSetup(webApplicationContext)
            .alwaysDo(JacksonResultHandlers.prepareJackson(objectMapper))
            .apply(
                MockMvcRestDocumentation.documentationConfiguration(restDocumentation)
                    .uris()
                    .withScheme("http")
                    .withHost("localhost")
                    .withPort(8080)
                    .and()
                    .snippets()
                    .withDefaults(
                        curlRequest(),
                        httpRequest(),
                        httpResponse(),
                        requestFields().failOnUndocumentedFields(false),
                        responseFields(),
                        pathParameters().failOnUndocumentedParams(true),
                        requestParameters().failOnUndocumentedParams(true),
                        description(),
                        methodAndPath(),
                        links(),
                        embedded(),
                        sectionBuilder()
                            .snippetNames(
                                SnippetRegistry.AUTO_METHOD_PATH,
                                SnippetRegistry.AUTO_DESCRIPTION,
                                SnippetRegistry.AUTO_PATH_PARAMETERS,
                                SnippetRegistry.AUTO_REQUEST_PARAMETERS,
                                SnippetRegistry.AUTO_REQUEST_FIELDS,
                                SnippetRegistry.HTTP_REQUEST,
                                SnippetRegistry.CURL_REQUEST,
                                SnippetRegistry.HTTP_RESPONSE,
                                SnippetRegistry.AUTO_EMBEDDED,
                                SnippetRegistry.AUTO_LINKS)
                            .skipEmpty(true)
                            .build()))
            .build();
  }
```

The actual test.

```
String endpoint = "users";
    String call = "signOnSuccess200";
 
    MockHttpServletRequestBuilder request =
        MockMvcRequestBuilders.post(EndpointConstants.PATH_USERS)
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(platformUserInboundDto));
 
    MvcResult mvcResult = mockMvc
        .perform(request)
        .andDo(
            document(
                endpoint + "/" + call,
                preprocessRequest(prettyPrint()),
                preprocessResponse(prettyPrint())))
        .andExpect(status().isOk())
        .andExpect(request().asyncStarted())
        .andDo(document(endpoint + "/" + call, preprocessRequest(prettyPrint())))
        .andReturn();
 
    mockMvc
        .perform(asyncDispatch(mvcResult))
        .andDo(document(endpoint + "/" + call, preprocessResponse(prettyPrint())));
```

## 4. AsciiDoctor Template
Create a file under /src/main/asciidoc named index.adoc and add the following content.

```
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toc-title: Index
:toclevels: 4
:sectlinks:
:sectnums:
:sectnumlevels: 5
:page-layout: docs
 
= IAM Services Documentation
 
This is the REST API documentation for User Services.
 
[[Signon]]
== SSO Signon and Platform User Creation
 
include::{snippets}/users/signOnSuccess200/auto-section.adoc[]
```

## 5. Running the Integration Tests
To run the integration tests execute the maven command: mvn clean install verify.

What happens then? When you run the integration tests, Spring REST Docs creates snippets with details from the JavaDoc like descriptions and constraints. These snippets are then inserted in the Asciidoctor template.

## 6. Example of Generated Documentation
This documentation is generated every time you run the integration test. Thus, it always gets the latest information from JavaDocs. No need for extra annotation like Swagger.

### 6.1 Spring REST Docs Snippets

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o2317hzymyo8cbiwmnnt.png)

### 6.2 REST Documentation

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5qoyzau4ndxkpabzoikq.png)

The description of this endpoint comes from JavaDoc. See the DTO from 3.1.

### 6.2 Example Requests

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u1sdlwz1kt49pe3s6gzp.png)

The request details are the values we use in our test case.

6.3 Example Response

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mtk7l9wmp38adpgeobjh.png)