Posts

Documenting REST API with Swagger in Spring Boot 3

Nov 9, 2023
Catherine Edelveis
14.4

API documentation is an indispensable part of developing web applications. Luckily, there are solutions that make meticulous manual documentation crafting a thing of the past.

This step-by-step tutorial will guide you through integrating Swagger (based on OpenAPI 3.0 specification) into a Spring Boot project.

By the way, if you deploy Spring Boot services to the cloud, check out Alpaquita Containers tailor-made for Spring Boot: they can help you save up to 30% RAM!

And did you know that you can easily reduce startup and warmup times of your Spring Boot services from minutes to milliseconds by using Java with CRaC support? Give it a try!

What is Swagger?

Swagger is a set of tools that help developers to create, edit, and use API documentation according to the OpenAPI specification. With Swagger, it is no longer necessary to manually write lengthy API docs: the solution is capable of reading the structure of the API you defined in the annotations of your code and automatically converting it into API specification.

What is more, Swagger provides a user interface that generates interactive API documentation that lets users test the API calls in the browser.

Main Swagger components are:

  • Swagger Editor for writing and editing API specs,
  • Swagger UI for creating interactive API documentation,
  • Swagger Codegen for generating server stubs or client libraries for your API.

Swagger vs OpenAPI

Both terms, Swagger and OpenAPI, are used in the context of API documentation, but they are not the same. OpenAPI is a standard specification for describing API, and Swagger helps to create API docs in line with this specification.

For example, BellSoft uses REST Discovery API based on OpenAPI specification to provide metadata about its products (version, build number, architecture, features, etc.) By querying this information, users can get a comprehensive understanding of product characteristics.

Integrating Swagger into a Spring Boot project

Prerequisites

  • JDK 21 or later (I will use Liberica JDK recommended by the Spring team)
  • Maven
  • Your favorite IDE (I will use IntelliJ IDEA)

Create a sample REST API project

Skip this step if you want to use your own project. You can follow along or implement the examples from the tutorial into your application accordingly.

The code for the demo project used below is available on GitHub.

Our demo Spring Boot application will expose REST APIs for managing employees. The Employee will have id, first name, and last name. Our APIs will allow for getting a list of all employees, getting one employee, adding a new employee, updating the existing employee, and deleting an employee according to the following schema:

 

Method

URL

Action

GET

/employees

Get a list of all employees

GET

/employees/{employeeId}

Get one employee by id

POST

/employees

Add an employee

PUT

/employees

Update an employee

DELETE

/employees/{employeeId}

Delete an employee

Head to Spring Initializr to create a skeleton of your project. Select Java, Maven, define a project name (I have openapidemo), and choose Java 21.

To save the effort and avoid winding up a local database, we will use the in-memory H2 database. We will also need Spring Web, Lombok, Spring Data JDBC, and DevTools dependencies. DevTools is a great time saver because you don’t have to restart the application every time you introduce changes, the recompilation is performed on the fly.

Generate the project and open it in your IDE.

Let’s keep the structure simple and yet close to production-like setup. First, we will need the basic class Employee:

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Employee {

    @Id
    private int id;

    private String firstName;

    private String lastName;

    public Employee(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

Let's also create DTOs so as not to pass around real database objects. For that, we will use a Mapstruct library. Here's the pom.xml with the Mapstruct dependency that you should add.

I won't go into details of working with MapStruct here. You can read more about creating DTOs with Mapstruct in my previous guide

The code for DTOs and Mapper:

@Builder
public record EmployeeDto(int id,
                          String firstName,
                          String lastName) {
}

@Builder
public record NewEmployeeDto(String firstName,
                             String lastName) {
}

@Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE,
        componentModel = "spring")
public class EmployeeMapper {


    public EmployeeDto mapToEmployeeDto(Employee employee) {

        return EmployeeDto.builder()
                .id(employee.getId())
                .lastName(employee.getLastName())
                .firstName(employee.getFirstName()).build();
    }

    public Employee mapToEmployee(EmployeeDto employeeDto) {
        return Employee.builder()
                .id(employeeDto.id())
                .firstName(employeeDto.firstName())
                .lastName(employeeDto.lastName())
                .build();
    }

    public Employee mapToEmployee(NewEmployeeDto employeeDto) {
        return Employee.builder()
                .firstName(employeeDto.firstName())
                .lastName(employeeDto.lastName())
                .build();
    }

}

Next, let's create EmployeeController, EmployeeRepository interface, and EmployeeService.

Populate the classes as shown below. I have also added a custom NotFoundException with relevant ErrorHandler.

public interface EmployeeRepository extends ListCrudRepository<Employee, Integer> { }
@Service
public class EmployeeService {

    private final EmployeeRepository employeeRepository;
    private final EmployeeMapper mapper;

    public EmployeeService(EmployeeRepository employeeRepository, EmployeeMapper employeeMapper) {
        this.employeeRepository = employeeRepository;
        this.mapper = employeeMapper;
    }

    public List<EmployeeDto> findAll() {
        return employeeRepository.findAll()
                .stream()
                .map(mapper::mapToEmployeeDto)
                .toList();
    }

    public EmployeeDto findById(int id) {
        Optional<Employee> employee = employeeRepository.findById(id);
        if (employee.isEmpty()) {
            throw new NotFoundException("Employee not found with id: " + id);
        }
        return mapper.mapToEmployeeDto(employee.get());
    }

    public EmployeeDto save(NewEmployeeDto employeeDto) {
        Employee employee = mapper.mapToEmployee(employeeDto);
        return mapper.mapToEmployeeDto(employeeRepository.save(employee));
    }

    public EmployeeDto update(EmployeeDto employeeDto) {
        Employee employee = mapper.mapToEmployee(employeeDto);
        return mapper.mapToEmployeeDto(employeeRepository.save(employee));
    }

    public void deleteById(int id) {
        if (!employeeRepository.existsById(id)) {
            throw new NotFoundException("Employee not found with id: " + id);
        }
        employeeRepository.deleteById(id);
    }
    
}
@RestController
public class EmployeeController {

    private final EmployeeService employeeService;

    public EmployeeController(EmployeeService service) {
        this.employeeService = service;
    }

    @GetMapping("/employees")
    public List<EmployeeDto> findAllEmployees() {
        return employeeService.findAll();
    }

    @GetMapping("/employees/{employeeId}")
    public EmployeeDto getEmployee(@Parameter(
            description = "ID of employee to be retrieved",
            required = true)
                                   @PathVariable int employeeId) {

        return employeeService.findById(employeeId);
    }

    @PostMapping("/employees")
    public EmployeeDto addEmployee(@RequestBody NewEmployeeDto employee) {
        return employeeService.save(employee);
    }

    @PutMapping("/employees")
    public EmployeeDto updateEmployee(@RequestBody EmployeeDto employee) {
        return employeeService.update(employee);
    }

    @DeleteMapping("/employees/{employeeId}")
    public String deleteEmployee(@PathVariable int employeeId) {
        employeeService.deleteById(employeeId);
        return "Deleted employee with id: " + employeeId;
    }

}

Now, let’s provide a database schema. Create a schema.sql file in the resources directory with the following content:

create table if not exists employee (
id serial primary key,
first_name varchar(255) not null,
last_name varchar(255) not null
);

Next, let’s populate our database instance with some data (that’s optional, we don’t need to work with DB data when developing APIs, I just don’t like to see the ugly error page upon starting the app in the browser). Create the data.sql file in resources with the following content:

delete from employee;
insert into employee (first_name, last_name) values ('John', 'Doe');
insert into employee (first_name, last_name) values ('Jane', 'Smith');

The last thing to do is to add several properties to the application.properties file because we are performing a script-based initialization:

database=h2
spring.sql.init.schema-locations=classpath*:schema.sql
spring.sql.init.data-locations=classpath*:data.sql
spring.sql.init.mode=always

Finally, let’s verify that the app is functioning as desired. Run the application.

You should see the following result at http://localhost:8080/employees:

[
  {
    "id": 1,
    "firstName": "John",
    "lastName": "Doe"
  },
  {
    "id": 2,
    "firstName": "Jane",
    "lastName": "Smith"
  }
]

That’s it! Our minimalistic CRUD application is ready for experiments.

Add springdoc-openapi dependency

To work with Swagger, we need the springdoc-api library that helps to generate OpenAPI-compliant API documentation for Spring Boot projects. The library supports Swagger UI and other useful features such as OAuth2 and GraalVM Native Image.

Add the following dependency for springdoc-api to your pom.xml file:

<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
  <version>2.2.0</version>
</dependency>

That’s all, no additional configuration is required!

Generate API documentation

The OpenAPI documentation is generated when we build our project. So let’s verify that everything is working correctly. Run your application and go to the default page where the API documentation is located: http://localhost:8080/v3/api-docs.

You should see the data on your endpoints in JSON format. You can also access the .yaml file at http://localhost:8080/v3/api-docs.yaml.

It is possible to change the default path in the application.properties file. For example:

springdoc.api-docs.path=/api-docs

Now the documentation is available at http://localhost:8080/api-docs.

Integrate Swagger UI

The beauty about springdoc-openapi library dependency is that it already includes Swagger UI, so we don’t have to configure the tool separately!

You can access Swagger UI at http://localhost:8080/swagger-ui/index.html, where you will see a beautiful user interface to interact with your endpoints (or similar to the one on the screenshot if you are using your project):

Swagger UI

Configure Swagger 3 in Spring Boot with annotations

Right now, our API documentation is not very informative. We can extend it with the help of annotations added to the application code. Below is the summary of the most common ones.

Add Swagger API description

First of all, let’s include some essential data about the API, such as name, description, and author contacts. For that purpose, create an OpenAPIConfiguration class and fill in the following code:

@Configuration
public class OpenAPIConfiguration {

   @Bean
   public OpenAPI defineOpenApi() {
       Server server = new Server();
       server.setUrl("http://localhost:8080");
       server.setDescription("Development");

       Contact myContact = new Contact();
       myContact.setName("Jane Doe");
       myContact.setEmail("[email protected]");

       Info information = new Info()
               .title("Employee Management System API")
               .version("1.0")
               .description("This API exposes endpoints to manage employees.")
               .contact(myContact);
       return new OpenAPI().info(information).servers(List.of(server));
   }
}

You can also fill in information about applicable License and some other data, but code above is enough for demonstration. Run the app and verify that the main API page includes provided information:

API description

Bean validation

The springdoc-openapi library supports JSR 303: Bean Validation (@NotNull, @Min, @Max, and @Size), so when we add these annotations to our code, the additional schema documentation will be automatically generated.

Let’s specify them in Employee class:

public class Employee {
   @Id
   private int id;

   @NotNull
   @Size(min = 1, max = 20)
   private String firstName;

   @NotNull
   @Size(min = 1, max = 50)
   private String lastName;

When you recompile your app, you will see that the Schemas section contains the specified info:

API Schema

@Tag annotation

The @Tag annotation can be applied at class or method level and is used to group the APIs in a meaningful way.

For instance, let’s add this annotation to our GET methods:

    @Tag(name = "get", description = "GET methods of Employee APIs")
    @GetMapping("/employees")
    public List<EmployeeDto> findAllEmployees() {
        return employeeService.findAll();
    }

    @Tag(name = "get", description = "Retrieve one employee")
    @GetMapping("/employees/{employeeId}")
    public EmployeeDto getEmployee(
                                   @PathVariable int employeeId) {

        return employeeService.findById(employeeId);
    }

You will see that APIs are now grouped differently:

API grouping

@Operation annotation

The @Operation annotation enables the developers to provide additional information about a method, such as summary and description.

Let’s update our updateEmployee() method:

    @Operation(summary = "Update an employee",
            description = "Update an existing employee. The response is updated Employee object with id, first name, and last name.")
    @PutMapping("/employees")
    public EmployeeDto updateEmployee(@RequestBody EmployeeDto employee) {
        return employeeService.update(employee);
    }

The API description in Swagger UI is now a little more informative:

Endpoint description

@ApiResponses annotation

The @ApiResponses annotation helps to add information about responses available for the given method. Each response is specified with @ApiResponse, for instance

    @ApiResponses({
            @ApiResponse(responseCode = "200", content = {@Content(mediaType = "application/json",
                    schema = @Schema(implementation = Employee.class))}),
            @ApiResponse(responseCode = "404", description = "Employee not found",
                    content = @Content)})
    @DeleteMapping("/employees/{employeeId}")
    public String deleteEmployee(@PathVariable int employeeId) {
        employeeService.deleteById(employeeId);
        return "Deleted employee with id: " + employeeId;
    }

After you recompile the Controller class, the data on responses will be automatically generated: 

API responses

@Parameter annotation

The @Parameter annotation can be used on a method parameter to define parameters for the operation. For example,

    public EmployeeDto getEmployee(@Parameter(
            description = "ID of employee to be retrieved",
            required = true)
                                   @PathVariable int employeeId) {

        return employeeService.findById(employeeId);
    }

Here, the description element provides additional data on parameter purpose, and required is set to true signifying that this parameter is mandatory.

And here’s how it looks in Swagger UI:

Endpoint parameters

Conclusion

As you can see, writing APIs with Swagger is extremely convenient. The solution enables declarative and consistent API documentation without laborious manual effort. As a result, it accelerates the development process and minimizes the risk of errors, so you should definitely integrate it into your workflow!

And if you are looking to optimize resourse consumption of your containerized Spring Boot apps, try out Alpaquita Containers tailor-made for Spring Boot and helping to achieve up to 30 % RAM savings!

 

Subcribe to our newsletter

figure

Read the industry news, receive solutions to your problems, and find the ways to save money.

Further reading