Enterprise Java

API Documentation with Spring REST Docs

In software development, creating clear and comprehensive documentation for APIs is essential. It ensures that developers can understand and utilize our APIs effectively. Also, regarding REST APIs, a significant aspect to consider is how to handle and document query parameters. However, maintaining accurate documentation that remains synchronized with evolving APIs can be challenging. Spring REST Docs is a tool that integrates documentation directly into our API development process, allowing us to produce concise and accurate documentation alongside our API implementation. This article will delve into the process of combining writing documentation and developing APIs using Spring REST Docs.

1. What is Spring REST Docs?

Spring REST Docs is an extension of the Spring Framework that facilitates the documentation of RESTful APIs. It leverages tests written with Spring MVC or WebFlux to produce accurate and up-to-date documentation. Instead of maintaining separate documentation files, Spring REST Docs generates documentation from our tests, ensuring that the documentation stays synchronized with our API implementation.

2. Getting Started with Spring REST Docs

To demonstrate how Spring REST Docs work, let’s create a simple Spring Boot project and document an API endpoint using Spring REST Docs.

2.1 Add Dependencies

Add the necessary dependencies for Spring REST Docs spring-restdocs-mockmvc in the pom.xml file.

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.restdocs</groupId>
            <artifactId>spring-restdocs-mockmvc</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

2.2 Define the Book Class

For this article, we’ll develop a basic REST service for managing book resources. Let’s begin by defining a Book domain class.

public class Book {
    private Long id;
    private String title;
    private String author;
    private String genre;

    // Constructors, getters, and setters
    public Book() {
    }

    public Book(Long id, String title, String author, String genre) {
        this.id = id;
        this.title = title;
        this.author = author;
        this.genre = genre;
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getGenre() {
        return genre;
    }

    public void setGenre(String genre) {
        this.genre = genre;
    }

    @Override
    public String toString() {
        return "Book{" + "id=" + id + ", title=" + title + ", author=" + author + ", genre=" + genre + '}';
    }
    
}

2.3 Create the BookController Class

Next, we create the controller class named BookController which returns an endpoint with a list of all Books.

@RestController
public class BookController {
    
    private List<Book> books = new ArrayList<>();
    
    public BookController() {
        // Sample data for books
        books.add(new Book(1L, "The Great Gatsby", "F. Scott Fitzgerald", "Classic"));
        books.add(new Book(2L, "To Kill a Mockingbird", "Harper Lee", "Classic"));
        books.add(new Book(3L, "1984", "George Orwell", "Dystopian"));
        books.add(new Book(4L, "The Age of Reason", "Thomas Paine", "Philosophy"));
        books.add(new Book(5L, "The Catcher in the Rye", "J.D. Salinger", "Coming-of-age"));
        books.add(new Book(6L, "Pride and Prejudice", "Jane Austen", "Romance"));
        books.add(new Book(7L, "Spring in Action", "Craig Walls", "Programming"));
    }

     @GetMapping("/api/books")
    public List<Book> getBooks(@RequestParam(required = false) String genre,
                               @RequestParam(required = false) String author) {
        // Filter books based on query parameters
        List<Book> filteredBooks = books;

        if (genre != null) {
            filteredBooks = filteredBooks.stream()
                    .filter(book -> book.getGenre().equalsIgnoreCase(genre))
                    .collect(Collectors.toList());
        }

        if (author != null) {
            filteredBooks = filteredBooks.stream()
                    .filter(book -> book.getAuthor().equalsIgnoreCase(author))
                    .collect(Collectors.toList());
        }

        return filteredBooks;
    }
}

In this BookController class:

  • The getBooks method is annotated with @GetMapping("/api/books") to handle GET requests to /api/books.
  • The method accepts optional query parameters genre and author using @RequestParam annotation.

3. Writing Documentation with Spring REST Docs

To ensure comprehensive API testing and documentation, we will utilize Spring REST Docs alongside MockMvc for generating documentation snippets.

3.1 Unit Test

Spring REST Docs leverages test cases to automatically generate accurate and updated documentation for our REST API. In this article, we’ll utilize JUnit 5 as the foundation for our test cases. Below is an example of a controller test class named BookControllerTest:

@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
@WebMvcTest(BookController.class)
public class BookControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testGetAllBooks() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/api/books"))
                .andExpect(status().isOk())
                .andExpect(MockMvcResultMatchers.content().string(containsString("Spring in Action")));
    }

}

In this BookControllerTest class, we use the annotation @ExtendWith({RestDocumentationExtension.class, SpringExtension.class})to configure the test to use both the Spring Extension and the RestDocumentationExtension. The SpringExtension.class integrates the Spring TestContext Framework into JUnit 5 tests, while RestDocumentationExtension.class supports documenting our API with Spring REST Docs.

We use MockMvc to perform a GET request to /api/books, verify that the response status is 200 OK with .andExpect(status().isOk()), and assert that the response from the /api/books endpoint contains a book title, specifically “Spring in Action”.

3.2 Using MockMvc for Documentation

When utilizing MockMvc for documenting Spring Boot APIs with Spring REST Docs, we integrate directly with the MVC framework to simulate HTTP requests and capture documentation snippets. MockMvc allows us to perform operations on our controllers and verify responses, ensuring that our API endpoints behave as expected.

First, we need to set up a MockMvc object to automatically generate default snippets for each test within the class and then write the unit test covering the execution of the api/books endpoint.

@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
@WebMvcTest(BookController.class)
public class BookControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    public void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
        this.mockMvc = webAppContextSetup(webApplicationContext)
                .apply(documentationConfiguration(restDocumentation))
                .alwaysDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
                .build();
    }

    @Test
    public void testGetAllBooks() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/api/books"))
                .andExpect(status().isOk())
                .andExpect(MockMvcResultMatchers.content().string(containsString("Spring in Action")));

    }
}

When we run this test, a new directory named generated-snippets will be generated within the build directory. Inside this directory, there will be a subfolder named after the test method, which will include the following asciidoc files:

Fig 1: Spring rest document using MockMvc generated output
Fig 1: Spring rest document using MockMvc generated output

We can enhance the documentation by incorporating a new step into the test using andDo(document(...)).

@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
@WebMvcTest(BookController.class)
@AutoConfigureRestDocs(outputDir = "target/generated-snippets")
public class BookControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testGetAllBooks() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/api/books"))
                .andExpect(status().isOk())
                .andExpect(MockMvcResultMatchers.content().string(containsString("Spring in Action")))
                .andDo(document("books"));
    }

}

In this updated code, we employ the @AutoConfigureRestDocs annotation, which specifies a directory location to store generated documentation snippets. When running the test with the command ./mvnw test and achieving a successful test result, a sub-folder will be created under target/generated-snippets. Within this folder, you will also discover a sub-directory named books, reflecting the argument used in the .andDo(document()) method. This books folder contains multiple .adoc files, as shown in Fig 2 below.

Fig 2: Spring rest document generation using .andDo(document()) method.
Fig 2: Spring rest document generation using .andDo(document()) method.

4. Documenting Query Parameters

Documenting query parameters in Spring REST Docs is essential for describing the purpose and usage of parameters in our API endpoints. To demonstrate documenting query parameters for the /api/books endpoint, we will incorporate query parameters for filtering books by genre and author.

4.1 Using MockMvc for Query Parameter Documentation

When using MockMvc to document query parameters, we can include the parameters in our request and capture the documentation using Spring REST Docs. Below is an example of how to document query parameters:

@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
@WebMvcTest(BookController.class)
@AutoConfigureRestDocs(outputDir = "target/generated-snippets")
public class BookControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    public void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
        this.mockMvc = webAppContextSetup(webApplicationContext)
                .apply(documentationConfiguration(restDocumentation))
                .alwaysDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
                .build();
    }

     @Test
    public void testGetBooksByGenreAndAuthor() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/api/books")
                .param("genre", "philosophy")
                .param("author", "Thomas Paine"))
                .andExpect(status().isOk())
                .andDo(document("books",
                        queryParameters(
                                parameterWithName("genre").description("Genre of the book"),
                                parameterWithName("author").description("Author of the book")
                        )
                ));
    }

}

In this MockMvc test, we perform a GET request to /api/books with query parameters genre and author. The queryParameters method is used to document these parameters with descriptions.

5. Converting Generated Documentation Snippets with AsciiDoctor Maven Plugin

After generating documentation snippets using Spring REST Docs, we can convert them into a readable format such as HTML or PDF using the AsciiDoctor Maven plugin. This plugin processes AsciiDoc files and produces human-readable output. The configuration of the AsciiDoctor plugin in our pom.xml file looks like this:

    <build>
        <plugins>
            <plugin>
                <groupId>org.asciidoctor</groupId>
                <artifactId>asciidoctor-maven-plugin</artifactId>
                <version>2.2.1</version>
                <executions>
                    <execution>
                        <id>generate-docs</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>process-asciidoc</goal>
                        </goals>
                        <configuration>
                            <backend>html</backend>
                            <doctype>book</doctype>
                            <sourceDirectory>src/doc/asciidocs</sourceDirectory>
                            <outputDirectory>${project.build.directory}/generated-docs</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
                <dependencies>
                    <dependency>
                        <groupId>org.springframework.restdocs</groupId>
                        <artifactId>spring-restdocs-asciidoctor</artifactId>
                        <version>${spring-restdocs.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

In the configuration above:

  • <backend> specifies the output format (HTML).
  • <doctype> sets the AsciiDoc document type.
  • <sourceDirectory> points to the directory containing generated snippets.
  • <outputDirectory> defines where the converted documentation will be saved.

5.1 Joining the Generated Snippets and Output

After generating documentation snippets using Spring REST Docs, we often need to consolidate these snippets into a single coherent document for easier readability and distribution. We need to create a new file called index.adoc within a src/doc/asciidocs directory to consolidate all the .adoc files into a single location. The index.adoc file looks like this:

==== This is the REST API documentation for Book Services.

==== CURL Sample

.request Book with CURL
include::{snippets}/books/curl-request.adoc[]

==== HTTP Request

.request
include::{snippets}/books/http-request.adoc[]

==== Query Parameters

.query-parameters
include::{snippets}/books/query-parameters.adoc[]

==== HTTP Response

.response
include::{snippets}/books/http-response.adoc[]

Next, execute the following Maven command to trigger the merging process and generate the consolidated output:

mvn clean package

This command will copy all .adoc files from the generated-snippets directory as specified in index.adoc file and converts them into an HTML file stored in the target/generated-docs directory (or as specified in <outputDirectory>). The generated HTML file (shown in Fig 2)will contain joined and formatted documentation as viewed from a web browser.

Fig 2. Output of generated documentation on a web browser
Fig 2. Output of generated documentation on a web browser

6. Conclusion

This article explored the basics of combining API development and documentation writing using Spring REST Docs. By writing tests that not only verify API behaviour but also generate documentation automatically, we can ensure that our documentation stays accurate and always reflects the current state of our API. This approach keeps our documentation up-to-date without additional manual effort.

7. Download the Source Code

This was an article on Spring Rest Document Query Parameters.

Download
You can download the full source code of this example here: Spring Rest Document Query Parameters

Omozegie Aziegbe

Omos holds a Master degree in Information Engineering with Network Management from the Robert Gordon University, Aberdeen. Omos is currently a freelance web/application developer who is currently focused on developing Java enterprise applications with the Jakarta EE framework.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button