Documentando uma API REST Spring Boot 3 usando OpenAPI 3.0
Tags: java, springboot, springweb, openapi
Published: 2024-01-09 00:00:45

## Visão Geral
A documentação é uma parte essencial da construção de APIs REST. Neste tutorial, veremos **SpringDoc**, que simplifica a geração e manutenção de documentos de **API** com base na especificação **OpenAPI** 3 para aplicativos **Spring Boot** 3.x. 

### Configuração

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

Spring Boot 3.x requer o uso da versão 2 do [springdoc-openapi](https://central.sonatype.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui):

```
ext {
	springdocVersion = "2.3.0"
}

dependencies {
	implementation "org.springframework.boot:spring-boot-starter-web"
	// ...
	implementation "org.springframework.boot:spring-boot-starter-validation"
	implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"
	implementation "org.springdoc:springdoc-openapi-starter-common:${springdocVersion}"
	// ...
	testImplementation "org.springframework.boot:spring-boot-starter-test"
}
```

Depois de configurar a dependência corretamente, podemos executar nossa aplicação e encontrar as descrições da **OpenAPI** em `/v3/api-docs`, que é o caminho padrão:

```
http://localhost:8080/v3/api-docs
```

Podemos personalizar o caminho em `application.properties` usando a propriedade `springdoc.api-docs`. Por exemplo, podemos definir o caminho para `/api-docs`:

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

Então, poderemos acessar a documentação na url:

```
http://localhost:8080/api-docs
```

As definições **OpenAPI** estão no formato **JSON** por padrão. Para o obter o formato `yaml`, podemos podemos acessar a url:

```
http://localhost:8080/api-docs.yaml
```

## Integração com UI Swagger

Além de gerar a especificação **OpenAPI** 3, podemos integrar `springdoc-openapi` com **Swagger UI** para interagir com nossa especificação de **API** e testar os endpoints. A dependência `springdoc-openapi` já inclui a **UI** do **Swagger**, então neste ponto, já podemos acessar a **Swagger UI** em:

```
http://localhost:8080/swagger-ui/index.html
```

### Support for swagger-ui Properties

A biblioteca `springdoc-openapi` também suporta propriedades `swagger-ui`. Elas podem ser usados ​​como propriedades **Spring Boot** com o prefixo `springdoc.swagger-ui`. Por exemplo, podemos personalizar o caminho da **Swagger UI** alterando a propriedade `springdoc.swagger-ui.path` dentro do nosso arquivo `application.properties`:

```
springdoc.swagger-ui.path=/documentation.html
```

Então podemos acessar a **Swagger UI** na url:

```
http://localhost:8080/documentation.html
```

## API de exemplo

```java
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.Collection;

@RestController
@RequestMapping("/api/books")
public class BookController {

    private final BookRepository repository;

    public BookController(BookRepository repository) {
        this.repository = repository;
    }

    @PostMapping
    public ResponseEntity<?> create(@RequestBody final Book payload, UriComponentsBuilder ucb) {
        Book book = repository.create(payload);

        var location = ucb
                .path("/movies/{id}")
                .buildAndExpand(book.getId())
                .toUri();

        return ResponseEntity.created(location).build();
    }

    @GetMapping("/{id}")
    public Book findById(@PathVariable final Long id) {
        return repository.findById(id)
            .orElseThrow(BookNotFoundException::new);
    }

    @GetMapping("/")
    public Collection<Book> findBooks() {
        return repository.getBooks();
    }

    @PutMapping("/{id}")
    @ResponseStatus(HttpStatus.OK)
    public ResponseEntity<?> update(@PathVariable("id") final Long id, @RequestBody final Book payload) {
        if (repository.findById(id).isEmpty()) return ResponseEntity.notFound().build();

        return ResponseEntity.accepted().body(repository.update(id, payload));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<?> delete(@PathVariable final Long id) {
        Book book = repository.findById(id).orElseThrow(BookNotFoundException::new);
        repository.delete(book);
        return ResponseEntity.noContent().build();
    }
}
```

### Repositório de exemplo

```java
import org.springframework.stereotype.Repository;
import java.util.*;

@Repository
public class BookRepository {

    private final Map<Long, Book> books = new HashMap<>();

    public Optional<Book> findById(final long id) {
        return Optional.ofNullable(books.get(id));
    }

    public Book create(Book book) {
        Long id = (long) (books.size() + 1);
        book.setId(id);
        books.put(id, book);
        return books.get(id);
    }

    public Collection<Book> getBooks() {
        return books.values();
    }

    public Book update(final long id, final Book book) {
        Book bookUpdate = books.get(id);
        bookUpdate.setAuthor(book.getAuthor());
        bookUpdate.setTitle(book.getTitle());
        books.put(id, bookUpdate);

        return books.get(id);
    }

    public void delete(final Book book) {
        books.remove(book.getId());
    }
}
```

### Modelo de exemplo

```java
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public class Book {
    private long id;
    private String title;
    private String author;

    // Getters and Setters
}

class BookNotFoundException extends RuntimeException {
}
```

## Application Properties de exemplo

```

# Open Api
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.operationsSorter=alpha
springdoc.api-docs.version=OPENAPI_3_0
spring.jpa.hibernate.ddl-auto=none

# Swagger
springdoc.swagger-ui.path=/documentation.html
```

## Testando
Então, se executarmos nossa API agora, podemos visualizar a documentação em:

```
http://localhost:8080/documentation.html
```

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

Aqui podemos usar a Swagger UI para testar nossa API fazendo:

- cadastro de novos livros
- obtendo todos os livros cadastrados
- obtendo um livro pelo id
- atualizando um livro cadastrado
- deletando um livro cadastrado

> Faça testes e explore a Documentação gerada automáticamente.

## Geração automática de documentos usando validação de bean JSR-303

Quando nosso modelo inclui anotações de validação de bean **JSR-303**, como `@NotNull`, `@NotBlank`, `@Size`, `@Min` e `@Max`, a biblioteca `springdoc-openapi` utiliza essas anotações para gerar documentação de esquema adicional para as restrições correspondentes. Vejamos um exemplo usando nosso bean Book:

```java
public class Book {

    private long id;

    @NotBlank
    @Size(min = 0, max = 20)
    private String title;

    @NotBlank
    @Size(min = 0, max = 30)
    private String author;

}
```

Percebe agora como ficou a documentação do Schema de Book:

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

## Gerando documentação adicional usando `@ControllerAdvice` e `@ResponseStatus`:

Usar `@ResponseStatus` em métodos em uma classe `@RestControllerAdvice` o openapi vai gerar automaticamente documentação para os códigos de resposta. Nesta classe `@RestControllerAdvice`, os dois métodos são anotados com `@ResponseStatus`:

```java
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalControllerExceptionHandler {

    @ExceptionHandler(ConversionFailedException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseEntity<String> handleConversion(RuntimeException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(BookNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ResponseEntity<String> handleBookNotFound(RuntimeException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
    }
}
```

As a result, we can now see the documentation for the response codes `400` and `404`:

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

## Gerando documentação usando `@Operation` e `@ApiResponses`

A seguir, vamos ver como podemos adicionar alguma descrição à nossa API usando algumas anotações da especificação **[OpenAPI](https://javadoc.io/doc/io.swagger.core.v3/swagger-annotations/latest/index.html)**.   
 
Para fazer isso, anotaremos o endpoint `/api/book/{id}` do nosso controlador com `@Operation` e `@ApiResponses`:

```java
@Operation(summary = "Get a book by its id")
@ApiResponses(value = { 
  @ApiResponse(responseCode = "200", description = "Found the book", 
    content = { @Content(mediaType = "application/json", 
      schema = @Schema(implementation = Book.class)) }),
  @ApiResponse(responseCode = "400", description = "Invalid id supplied", 
    content = @Content), 
  @ApiResponse(responseCode = "404", description = "Book not found", 
    content = @Content) })
@GetMapping("/{id}")
public Book findById(@Parameter(description = "id of book to be searched") 
  @PathVariable long id) {
    return repository.findById(id).orElseThrow(() -> new BookNotFoundException());
}
```

Agora nosso endpoint `/api/book/{id}` ficou documentado assim:

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

## Conclusão

Neste artigo, aprendemos como configurar o `springdoc-openapi` em nossos projetos. Em seguida, vimos como integrar `springdoc-openapi` com a **UI do Swagger**. 

Depois disso, vimos como o `springdoc-openapi` gera documentação automaticamente usando anotações de validação de bean JSR 303 e as anotações `@ResponseStatus` na classe `@ControllerAdvice`.

Também aprendemos como adicionar uma descrição à nossa API usando algumas anotações específicas da **OpenAPI**

O `springdoc-openapi` gera documentação da API de acordo com as especificações do **OpenAPI 3**. Além disso, ele também cuida da configuração da **UI do Swagger** para nós, tornando a geração de documentos da API uma tarefa razoavelmente simples.

### Referencias

- [Documenting a Spring REST API Using OpenAPI 3.0 - Baeldung](https://www.baeldung.com/spring-rest-openapi-documentation)
- [Spring Framework Documentation](https://docs.spring.io/spring-framework/reference/index.html)]
- [springdoc-openapi v2.3.0](https://springdoc.org/#groovy-support)
- [OpenAPI Specification v3.1.0](https://spec.openapis.org/oas/latest.html)
- [OpenAPI Specification](https://swagger.io/specification/)