API documentation and testing is easy with Swagger!

Adam Zink
10 min readSep 25, 2018

Now that the User API has a complete set of operations, one of the next logical steps will be to incorporate the endpoints into a custom UI (User Interface) and allow modification of Users without needing a REST client like Postman.

However, it’s not desired to require UI developers to read and understand Spring Boot code in order to call the API. The key part of API is interface, meaning there should be a stable, convenient way of communicating the operation names and behaviors.

Swagger is an industry standard set of tools to document an API. For Spring Boot, it provides annotations to describe API resources. Then, a build plugin can read the annotations at compile time and generate a Swagger UI to display the available operations. The Swagger UI also provides a convenient “Try it out” feature to send requests and get real responses.

Audience

Anyone who wants to learn how to integrate Swagger with Spring Boot applications.

Experience with Java programming and Maven configuration is helpful but not required.

Requirements

Background

Before we begin, I want to add a quick note about the OpenAPI Initiative and its relatively new OpenAPI Specification (OAS).

Initially, I planned to use the latest OpenAPI 3.0 spec so this tutorial would be relevant for longer than the current Swagger 2 spec. However, I found Swagger 2 is still much better supported in the full Java ecosystem of plugins.

In particular, the most important plugin for me was the swagger-maven-plugin, which does not support OpenAPI 3.0 as of writing this in September 2018, so I chose Swagger 2 instead.

Springfox

We will use Springfox maven dependencies to get access to both Swagger annotations and the Swagger UI features.

Add two new artifacts within the <dependencies>...</dependencies> section of pom.xml:

<!-- Added for Part 4 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>

Then, add a new file, SwaggerConfiguration.java:

@Configuration
@EnableSwagger2
@Primary
public class SwaggerConfiguration implements SwaggerResourcesProvider, WebMvcConfigurer {

@Override
public List<SwaggerResource> get() {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setLocation("/swagger/swagger.json");
swaggerResource.setName("Spring Boot MySQL Demo API");

return Collections.singletonList(swaggerResource);
}

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");

registry.addResourceHandler("/swagger/**")
.addResourceLocations("classpath:/swagger/");
}
}
  • @EnableSwagger2 formally indicates that Swagger should be enabled. It belongs in a class that also has @Configuration annotation
  • @Primary annotation is necessary to mark this configuration as the one to use. If missing, the configuration may conflict with defaults and cause an error like “ApiResourceController required a single bean, but 2 were found: …”
  • The get() method sets some of the properties for Swagger. The location, /swagger/swagger.json, is where Swagger UI will expect to find details of the API operations
  • The addResourceHandlers() method is for mapping resources used by the Swagger UI, including swagger.json

Maven

At this point, if you have been running the application directly in your IDE like IntelliJ, I recommend becoming familiar with building the application with Maven commands (either using a terminal within the IDE or using command prompt). Maven provides a lot more flexibility and generates a JAR (Java ARchive) file which conveniently packages all files needed to run the application. Swagger will need Maven in order to generate the UI properly.

In the root directory of the project, run the following command to generate the JAR in a target/ folder:

mvn clean install

Then, run the JAR with this command:

java -jar target/spring-boot-mysql-demo-0.0.1-SNAPSHOT.jar

Once the application starts, you can access Swagger UI at the following URL: http://localhost:8080/swagger-ui.html

However, as you can see below, the definition of the API operations is missing.

Swagger Annotations

Although the configuration allowed us to add this HTML to our project with remarkable ease, the majority of the work is still left to do. The benefit of Swagger comes from annotating individual Spring Boot resources, considering the expected input and describing possible responses. To achieve this, annotations are placed at the class level with @Api and at the endpoint level with @ApiOperation.

To start, add a simple @Api annotation to UserResource with value (i.e. name) and description:

@Path("/users")
@Api(value = "User", description = "Resource for getting and modifying Users")
@Component
public class UserResource {
...

Next, we need to tell Maven to look at the Swagger annotations when it builds the project. The swagger-maven-plugin interprets the annotations and we can configure it to write swagger.json to target/classes/swagger folder.

If you are not familiar with Maven, it regenerates the target/ folder and all of its contents each time you run mvn clean install (specifically when running with clean option, which is exactly what we want to get the latest Swagger definition each time the project is built).

Add a new plugin within the <plugins>...</plugins> section of pom.xml:

<plugin>
<groupId>com.github.kongchen</groupId>
<artifactId>swagger-maven-plugin</artifactId>
<configuration>
<apiSources>
<apiSource>
<locations>
<location>com.github.adamzink.springbootmysqldemo.resource</location>
</locations>
<basePath>/api</basePath>
<swaggerDirectory>${project.build.directory}/classes/swagger</swaggerDirectory>
<swaggerFileName>swagger</swaggerFileName>
<info>
<title>Spring Boot MySQL Demo</title>
<version>1.0</version>
</info>
</apiSource>
</apiSources>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
  • Note how the location set in SwaggerConfiguration matches the configuration for the build plugin. The directory is swagger/ and filename is swagger (with .json on the end since the generated file is JSON format)
  • The base path is set to /api to indicate the prefix on all the endpoints

Revisiting Prefix for API Endpoints

While getting Swagger to work, I found out there are different ways to set the path prefix in Spring Boot, and the way I used in Part 2 does not play well with Swagger UI. Until the problem was fixed, I got a persistent error saying “Unable to infer base url”. A preferred way is to annotate JerseyConfiguration with the following:

@ApplicationPath("/api")

Then, remove the old lines from application.properties:

# Path properties
server.servlet.contextPath=/api

This way, only the API endpoints are prefixed, and Swagger UI is able to access its own static resources.

If you run the application again, Swagger UI loads like below:

Once again, minimal configuration has already yielded a good result! The endpoints are showing up correctly, although clicking an individual endpoint like POST shows it is missing details such as parameters. Next, we will review each of the four operations and add the appropriate documentation.

Documentation for GET

The simplest operation to document is getting all Users. It takes no parameters because we want all Users every time. All we need is a description of what it does and details about the response object.

Add this above the getAll() method in UserResource:

@ApiOperation(value = "Get all Users",
response = User.class,
responseContainer = "List")
  • @ApiOperation is the most basic Swagger annotation to describe a method
  • response is the name of a Java class whose data will be returned. Note the JSON representation of the Java class is what actually gets returned
  • If anything other than a single object is returned, use theresponseContainer parameter to tells Swagger what to expect

Each time the Swagger annotations are modified, make sure to run mvn clean install again to get the latest changes.

Then, going back to Swagger UI, click Try it out to expand the input panel. We can use it to return the actual Users in MySQL, which is very useful for testing and helping others understand how the API works.

Since no parameters are needed, click Execute:

The request worked and an empty list [] was returned since the only User was deleted at the end of Part 3. We can try it again after adding another User below.

Documentation for POST

Creating a User requires a description of the request object. It is also useful to define the possible error conditions, especially for HTTP methods which are supposed to modify data like POST, so the requester can handle all situations gracefully.

Add the following above the save() method in UserResource:

@ApiOperation(value = "Create a User",
response = User.class)
@ApiResponses(value = {
@ApiResponse(code = 400, message = "Bad request format")})
public User save(
@ApiParam(value = "User to be created", required = true) final UserRequest userRequest) {
return userService.save(userRequest);
}
  • @ApiParam describes each parameter for the operation. In this case, the only parameter is the User request object
  • There is one @ApiResponse for each HTTP status code that we want to describe. All responses are grouped under @ApiResponses

In order to guarantee the 400 status code is returned for an incorrectly formatted request, let’s add a private helper method in UserService to validate the request object:

private UserModel getValidatedRequestToModel(final UserRequest userRequest) {
UserModel model = userConverter.requestToModel(userRequest);

if (userRequest.getFirstName() == null || userRequest.getLastName() == null) {
throw new BadRequestException();
}

return model;
}
  • Call the converter’s requestToModel method
  • If either First Name or Last Name in the model is null, thenjavax.ws.rs.BadRequestException is thrown, which causes the response to have 400 status code

Then, update the save() method in UserService to do the validation:

public User save(final UserRequest userRequest) {
UserModel userModel = getValidatedRequestToModel(userRequest);

userModel.setAddTs(new Date());

return userConverter.modelToResponse(userRepository.save(userModel));
}

After regenerating Swagger UI, click Try it out and enter body like below:

{
"firstName": "Adam",
"lastName": "Zink"
}

Then, click Execute:

To test the error handling, try submitting an incomplete request body like this:

{
"firstName": "Adam"
}

As expected, the 400 status is returned with “Bad Request” message.

Documentation for PUT

In addition to the request object, PUT requests also need the id of the User. Another response code can be added to handle the scenario where User is not found.

Add the following above the update() method in UserResource:

@ApiOperation(value = "Update a User",
response = User.class)
@ApiResponses(value = {
@ApiResponse(code = 400, message = "Bad request format"),
@ApiResponse(code = 404, message = "User not found")})
public User update(
@ApiParam(value = "User id to update", required = true) @PathParam("id") final Long id,
@ApiParam(value = "User details to be updated", required = true) final UserRequest userRequest) {
return userService.update(id, userRequest);
}
  • In addition to the handler for bad request, there is another@ApiResponse for handling when id is not found
  • @ApiParam goes on both parameters, regardless of their location in the request (Id is a path parameter, and the User parameter is part of the request body)

Then, modify the update() method in UserService to validate parameters:

public User update(final Long id, final UserRequest userRequest) {
UserModel toSave = userRepository.findById(id).orElseThrow(NotFoundException::new);

UserModel fromRequest = getValidatedRequestToModel(userRequest);

toSave.setFirstName(fromRequest.getFirstName());
toSave.setLastName(fromRequest.getLastName());

return userConverter.modelToResponse(userRepository.save(toSave));
}
  • Before doing anything else, the id is validated against the database. If the id is missing, the javax.ws.rs.NotFoundException is thrown and no further validation takes place
  • Even if the id is found, the User request details must be valid for the update to be saved

After regenerating Swagger UI, click Try it out and enter details like below with a User id that exists:

Then, click Execute to update the User’s name:

To test the new error handling when id is not found, change the id parameter to something like 999, and click Execute again:

As expected, the 404 status is returned with “Not Found” message.

Documentation for DELETE

The delete request has similarities to PUT, but it is simpler because only the id path parameter is required. One additional response status, 204, is also added to indicate successful deletion of the User by returning No Content.

Add the following above the delete() method in UserResource:

@ApiOperation(value = "Delete a User")
@ApiResponses(value = {
@ApiResponse(code = 204, message = "User deleted successfully"),
@ApiResponse(code = 404, message = "User not found")})
public void delete(
@ApiParam(value = "User id to delete", required = true) @PathParam("id") final Long id) {
userService.delete(id);
}

Then, modify the delete() method in UserService to validate id to be deleted:

public void delete(final Long id) {
userRepository.findById(id).orElseThrow(NotFoundException::new);
userRepository.deleteById(id);
}

After regenerating Swagger UI, click Try it out and enter a User id that exists (like 1), and then click Execute:

To verify the User was deleted, keep the id as 1 and click Execute again:

As expected, the 404 status is returned with “Not Found” message.

The User API is now well documented, easy to test, and ready to be integrated into any number of custom applications!

--

--