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 handleGET
requests to/api/books
. - The method accepts optional query parameters
genre
andauthor
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:
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.
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.
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.
You can download the full source code of this example here: Spring Rest Document Query Parameters