API REST com Spring + Testes com MockMVC + Documentação com Swagger
Tags: java, ptbr, restapi, spring
Published: 2019-12-18 17:03:48

---
title: API REST com Spring + Testes com MockMVC + Documentação com Swagger
published: true
date: 2019-08-08 22:45:20 UTC
tags: java,ptbr,restapi,spring
canonical_url: https://www.paulocollares.com.br/programacao/api-rest-com-spring-testes-de-integracao-com-mockmvc-documentacao-com-swagger/
---

## Introdução

Este post tem como objetivo desenvolver uma API REST 100% funcional com Spring e banco de dados PostgreSQL, o Spring data JPA para facilitar as consultas, o padrão DTO para inclusão e atualização dos dados, listar grandes quantidades de dados paginas, com ordenação e busca, implementar testes de integração para validar nossos endpoints com o MockMVC e gerar a documentação de forma automática com o Swagger.

O código completo pode ser obtido no Github:

[Clone no Github  
](https://github.com/pcollares/api-rest-spring)

## Dependências

As dependências na nossa aplicação são:

- Spring Boot
- Hibernate
- Spring data JPA
- MockMVC
- Swagger

Todas serão tratadas pelo Maven, segundo o pom.xml abaixo:

```
<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>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.6.1</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.6.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>
    </dependencies>
```

## Spring boot

O Spring boot dispensa a necessidade de um servidor de aplicação (explicito) para executar nossa aplicação, facilitando a execução durante o desenvolvimento e até em produção.

Para isso basta criarmos uma classe com o método main do Java:

```
@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}
```

## Hibernate e Spring data JPA

Vamos utilizar o Hibernate em conjunto do Spring data JPA para automatizar o mapeamento de nossas classes de domínio.

Primeiramente, vamos criar uma classe Produto.java e usar as anotações necessárias para realizar o mapeamento.

```
As anotações de @ApiModelProperty serão usadas para a documentação com o Swagger, que veremos mais a frente.

@Entity
@Table(name = "produto")
public class Produto {

    @Id
    @SequenceGenerator(name = "produto_seq", sequenceName = "produto_seq", allocationSize = 1)
    @GeneratedValue(generator = "produto_seq", strategy = GenerationType.AUTO)
    private int id;

    private String nome;

    private double valor;

    @ApiModelProperty(notes = "Identificador do produto")
    public int getId() {
        return id;
    }

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

    @ApiModelProperty(notes = "Nome do produto")
    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    @ApiModelProperty(notes = "Valor do produto")
    public double getValor() {
        return valor;
    }

    public void setValor(double valor) {
        this.valor = valor;
    }

}
```

Depois criaremos um ‘Repository’, estrutura do Spring data JPA que facilita ainda mais as consultas ao banco de dados, além de ser útil mais a frente durante os testes, onde será possível criar uns repositórios falsos para que não seja necessário acessar o banco de dados durante essa etapa.

No repositório de produto abaixo, criamos dois métodos, um _findAll()_ passando um objeto de paginação, somente pelo nome do método, será inferido uma busca por todos os elementos, e um método de busca com uma consulta personalizada.

```
O 'Repository' são interfaces, que o Spring irá tratar como injeção de dependências quando forem invocadas mais a frente.

@Repository
public interface ProdutoRepository extends PagingAndSortingRepository<Produto, Integer> {

    public Page<Produto> findAll(Pageable pageable);

    @Query("SELECT p FROM Produto p "
            + "WHERE lower(nome) like %:busca% ")
    public Page<Produto> busca(@Param("busca") String busca, Pageable pageable);

}
```

Edite o arquivo application.properties com os dados da sua conexão, configurações da api, como o caminho padrão e o modo de inicialização. Devemos criar também o banco de dados e um esquema no PostgreSQL.

```
#debug
debug=true

#api
server.servlet.context-path=/api

#conexão
javax.persistence.create-database-schemas=true
spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/teste
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.jpa.properties.hibernate.default_schema=api

#define como o hibernate irá se comportar quanto a criação do esquema
#create: Apaga e recria todo o esquema
#update: Atualiza o mapeamento
spring.jpa.hibernate.ddl-auto=update

#define se irá executar o 'data.sql'
#always: Sempre irá executar o data.sql
#never: Nunca irá executar o data.sql
spring.datasource.initialization-mode=never

#codificação do data.sql
spring.datasource.sqlScriptEncoding=UTF-8
```

## Controlador REST

Para disponibilizar nossa API publicamente, vamos criar uma classe ProdutoController.java para colocar todos os endpoints referentes a essa classe. E com as anotações necessárias do Spring.

A anotação _@RequestMapping(“/produtos”)_ irá definir o caminho padrão desse endpoint, nesse caso /produtos.

Para permitir a invocação de nossos endpoint de outras fontes, como uma aplicação REACT ou Angular, precisamos habilitar o CORS (Cross-Origin Resource Sharing), com a anotação _@CrossOrigin._

```
As anotações @Api e @ApiOperation servirão para documenta pelo swagger, que veremos mais a frente.

@RestController
@RequestMapping("/produtos")
@CrossOrigin
@Api(tags = "Produtos", description = "API de produtos")
public class ProdutoController {

    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    @Autowired
    ProdutoRepository produtoRepository;

...

}
```

Acima, temos a estrutura básica de um controlador rest, com as anotações necessárias, preparo uma instância do Logger e uma do Repositório de produtos, que será construído por injeção de dependências pelo próprio Spring.

### GET

O primeiro endpoint que vamos criar, será o responsável pela resposta da consulta por todos os produtos, pela anotação @GetMapping() informo que responderá a uma requisição do tipo GET. Defino os parâmetros que essa URL pode receber, todos opcionais, para que possa paginar, ordenar e fazer uma busca.

```
@ApiOperation(value = "Lista os produtos")
    @GetMapping()
    public Page<Produto> listar(
            @RequestParam(
                    value = "page",
                    required = false,
                    defaultValue = "0") int page,
            @RequestParam(
                    value = "size",
                    required = false,
                    defaultValue = "10") int size,
            @RequestParam(
                    value = "sort",
                    required = false) String sort,
            @RequestParam(
                    value = "q",
                    required = false) String q
    ) {
        Pageable pageable = new PageableFactory(page, size, sort).getPageable();

        Page<Produto> resultPage;

        if (q == null) {
            resultPage = produtoRepository.findAll(pageable);
        } else {
            resultPage = produtoRepository.busca(q.toLowerCase(), pageable);
        }

        return resultPage;
    }

Preciso criar um objeto Pageable para passar para o repositório, para isso criei uma fábrica para facilitar essa criação, a PageableFactory.
```

Abaixo uma consulta de exemplo para a URL /api/produtos:

```
{
  "content": [
    {
      "id": 2,
      "nome": "Processador Intel Core i7-9700K",
      "valor": 2454
    },
    {
      "id": 3,
      "nome": "Headset Gamer HyperX Cloud Stinger - HX-HSCS-BK/NA ",
      "valor": 189.37
    },
    {
      "id": 4,
      "nome": "Teclado Mecânico Gamer HyperX Mars, RGB, Switch Outemu Bluem, US - HX-KB3BL3-US/R4 ",
      "valor": 284.11
    },
    {
      "id": 5,
      "nome": "Mouse Logitech M90 Preto 1000DPI ",
      "valor": 26.9
    },
    {
      "id": 6,
      "nome": "Gabinete C3Tech Gamer ATX sem Fonte Preto MT-G50BK",
      "valor": 119.6
    },
    {
      "id": 7,
      "nome": "Headphone Edifier Bluetooth W800BT Preto",
      "valor": 250
    },
    {
      "id": 8,
      "nome": "Kindle Novo Paperwhite, 8GB, Wi-Fi, Preto - AO0705 ",
      "valor": 418.99
    },
    {
      "id": 9,
      "nome": "SSD Kingston A400, 240GB, SATA, Leitura 500MB/s, Gravação 350MB/s - SA400S37/240G ",
      "valor": 166
    },
    {
      "id": 10,
      "nome": "HD Seagate BarraCuda, 1TB, 3.5´, SATA - ST1000DM010",
      "valor": 290
    },
    {
      "id": 11,
      "nome": "Cadeira Gamer DT3sports GT, Black - 10293-5",
      "valor": 552.41
    }
  ],
  "pageable": {
    "sort": {
      "unsorted": true,
      "sorted": false,
      "empty": true
    },
    "pageSize": 10,
    "pageNumber": 0,
    "offset": 0,
    "unpaged": false,
    "paged": true
  },
  "last": false,
  "totalPages": 2,
  "totalElements": 19,
  "numberOfElements": 10,
  "sort": {
    "unsorted": true,
    "sorted": false,
    "empty": true
  },
  "first": true,
  "size": 10,
  "number": 0,
  "empty": false
}
```

Alguns exemplos de outras consultas possíveis:

- http://127.0.0.1:8080/api/produtos?page=2
- http://127.0.0.1:8080/api/produtos?page=1&size=30
- http://127.0.0.1:8080/api/produtos?q=teclado
- http://127.0.0.1:8080/api/produtos?sort=valor,asc
- http://127.0.0.1:8080/api/produtos?sort=valor,desc
- http://127.0.0.1:8080/api/produtos?page=0&size=3&ort=valor,desc&q=intel

Outro endpoint útil é a listagem de um item pelo seu id, que será feito assim:

```
@ApiOperation(value = "Busca um produto pelo id")
    @GetMapping(value = "/{id}")
    public ResponseEntity<Produto> listar(@PathVariable Integer id) {
        Optional<Produto> rastreador = produtoRepository.findById(id);

        if (!rastreador.isPresent()) {
            return ApiError.notFound("Produto não encontrado");
        }

        return new ResponseEntity<>(rastreador.get(), HttpStatus.OK);
    }
```

### POST e PUT

Para inserir ou atualizar um novo produto, vamos primeiramente criar uma classe ProdutoDTO.java.

```
O padrão DTO (Data transfer object) é uma classe que representa a entidade com apenas os atributos necessários para serem expostos publicamente, no nosso exemplo apenas preciso, na criação ou atualização, informar no nome e valor do produto, e nunca o seu id, por isso a sua classe de DTO não possui esse atributo

public class ProdutoDTO {

    private String nome;
    private Double valor;

    @ApiModelProperty(notes = "Nome do produto")
    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    public Double getValor() {
        return valor;
    }

    @ApiModelProperty(notes = "Valor do produto")
    public void setValor(Double valor) {
        this.valor = valor;
    }
}
```

Sendo assim, o método que representa essa criação ficará assim:

```
@ApiOperation(value = "Cria um novo Produto")
    @PostMapping()
    public ResponseEntity<Produto> criar(@RequestBody ProdutoDTO dto, UriComponentsBuilder ucBuilder) {
        try {
            //Crio um objeto da entidade preenchendo com os valores do DTO e validando
            Produto produto = new Produto();

            if (dto.getNome() == null || dto.getNome().length() < 2) {
                return ApiError.badRequest("Informe o nome do produto");
            }
            produto.setNome(dto.getNome());

            if (dto.getValor() == null || dto.getValor() <= 0) {
                return ApiError.badRequest("Valor do produto inválido");
            }
            produto.setValor(dto.getValor());

            Produto novo = produtoRepository.save(produto);

            //Se ocorreu algum erro, retorno esse erro para a API
            if (novo == null) {
                return ApiError.badRequest("Ocorreu algum erro na criação do produto");
            }

            //Se foi criado com sucesso, retorno o objeto criado
            return new ResponseEntity<>(novo, HttpStatus.CREATED);
        } catch (Exception e) {
            LOGGER.error("Erro ao criar um produto", e);
            return ApiError.internalServerError("Ocorreu algum erro na criação do produto");
        }
    }
```

Repare que informo que o ProdutoDTO é o recurso esperado, logo, quando fizer essa requisição, devo passar um json com esses atributos, por exemplo:

```
{
"nome": "Teclado Microsoft",
"valor": 124
}
```

Ainda nesse método, faço as validações necessárias, retornando um erro quando não forem atendidos os requisitos, e por final chamo o repositório pedindo para persistir o novo produto.

No final retorno o produto criado. Esse processo pode ser visto abaixo.

![](https://www.paulocollares.com.br/wp-content/uploads/2019/08/post-1024x439.png)

A atualização de um produto segue a mesma lógica, informando na URL o id do mesmo.

```
@ApiOperation(value = "Atualiza um Rastreador Equipamento")
    @PutMapping(value = "/{id}")
    public ResponseEntity<Produto> atualizar(@PathVariable("id") int id, @RequestBody ProdutoDTO dto) {
        try {
            Optional<Produto> produtoAtual = produtoRepository.findById(id);

            if (!produtoAtual.isPresent()) {
                return ApiError.notFound("Produto não encontrado");
            }

            if (dto.getNome() != null) {
                if (dto.getNome().length() < 2) {
                    return ApiError.badRequest("Nome do produto inválido");
                }
                produtoAtual.get().setNome(dto.getNome());
            }

            if (dto.getValor() != null) {
                if (dto.getValor() <= 0) {
                    return ApiError.badRequest("Valor do produto inválido");
                }
                produtoAtual.get().setValor(dto.getValor());
            }

            //Atualizo o objeto utilizando o repositório
            Produto atualizado = produtoRepository.save(produtoAtual.get());

            //Se ocorreu algum erro, retorno esse erro para a API
            if (atualizado == null) {
                return ApiError.internalServerError("Erro na atualização do produto");
            }

            //Se foi criado com sucesso, retorno o objeto atualizado
            return new ResponseEntity<>(atualizado, HttpStatus.CREATED);
        } catch (Exception e) {
            LOGGER.error("Erro ao atualizar um produto", e);
            return ApiError.internalServerError("Erro na atualização do produto");
        }
    }
```

## DELETE

A remoção também é bem simples, recebo o id, verifico se existe, e retorno um erro se não existir e removo pelo repositório.

```
@ApiOperation(value = "Remove um produto")
    @DeleteMapping(value = "/{id}")
    public ResponseEntity<Produto> deletar(@PathVariable Integer id) {
        Optional<Produto> produto = produtoRepository.findById(id);

        if (!produto.isPresent()) {
            return ApiError.notFound("Produto não encontrado");
        } else {
            produtoRepository.deleteById(id);
        }

        return new ResponseEntity<>(HttpStatus.OK);
    }
```

## Documentação com Swagger

O Swagger irá gerar uma documentação da nossa API de forma automática, olhando as anotações que fizemos nos controladores e nas classes de entidade e DTO.

Para isso vamos criar uma classe SwaggerConfig com as anotações necessárias e informando o pacote onde se encontram os nossos controladores.

```
@Configuration
@EnableSwagger2
public class SwaggerConfig extends WebMvcConfigurationSupport {

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(apis())
                .paths(PathSelectors.any())
                .build();
    }

    private Predicate<RequestHandler> apis() {
        return RequestHandlerSelectors.basePackage("br.com.paulocollares.api.controladores.rest");
    }

    private ApiInfo apiInfo() {

        return new ApiInfoBuilder()
                .title("SPRING REST API")
                .description("Documentação das APIs REST")
                .contact(new Contact("pcollares", "www.paulocollares.com.br", null))
                .build();
    }

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

        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}
```

Acessando a URL /api/swagger-ui.html, podemos ver a documentação gerada, como no exemplo abaixo.

![](https://www.paulocollares.com.br/wp-content/uploads/2019/08/Screenshot_2019-08-08-Swagger-UI-1024x593.png)

```
No projeto há uma classe, MainController, que redireciona a requisição da raiz para essa página, ou seja, se acessar http://127.0.0.1:8080/api/ é redirecionado para http://127.0.0.1:8080/api/swagger-ui.html.
```

## Testes

Para testar todos esses endpoins vamos usar o MockMVC para automatizar esse processo. Ele será responsável por invocar e testar o retorno das requisições.

A estrutura inicial da classe ProdutoTest inicia o MockMVC.

```
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {
    ProdutoController.class
})
public class ProdutoTest {

    //URL base para acesso desse controlador
    private final String BASE_URL = "/produtos";

    //Instância do ObjectMapper para trabalhar com JSON
    private ObjectMapper objectMapper;

    //Controlador REST tratado por meio de injeção de dependências
    @Autowired
    private ProdutoController restController;

    //Instância do MockMVC
    private MockMvc mockMvc;

    //Instância do mock repository
    @MockBean
    private ProdutoRepository mockRepository;

    @Before
    public void setUp() {
        objectMapper = new ObjectMapper();
        mockMvc = MockMvcBuilders
                .standaloneSetup(restController)
                .build();
    }

...

}
```

Vamos fazer um teste ao endpoint de consulta de um produto, para que não seja necessário acessar p banco de dados, e ter dados salvos no mesmo para realizar os testes, utilizamos do mockito para responder uma requisição ao repositório com um dado falso, e verificamos o retorno do endpoint em si.

```
@Test
    public void buscar_id_200() throws Exception {

        Produto produto = new Produto();
        produto.setId(1);
        produto.setNome("Teste");
        produto.setValor(10.0);

        when(mockRepository.findById(1)).thenReturn(Optional.of(produto));

        mockMvc.perform(get(BASE_URL + "/1"))
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id", is(1)))
                .andExpect(jsonPath("$.nome", is("Teste")))
                .andExpect(jsonPath("$.valor", is(10.0)));

        verify(mockRepository, times(1)).findById(1);
    }
```

Essa mesma lógica será usada nos outros endpoins.

```
@Test
    public void buscar_id_404() throws Exception {
        mockMvc.perform(get(BASE_URL + "/2")).andExpect(status().isNotFound());
    }

    @Test
    public void criar_200() throws Exception {

        ProdutoDTO dto = new ProdutoDTO();
        dto.setNome("Teste");
        dto.setValor(11.0);

        Produto produto = new Produto();
        produto.setId(1);
        produto.setNome(dto.getNome());
        produto.setValor(dto.getValor());

        when(mockRepository.save(any(Produto.class))).thenReturn(produto);

        mockMvc.perform(post(BASE_URL)
                .content(objectMapper.writeValueAsString(dto))
                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id", is(1)))
                .andExpect(jsonPath("$.nome", is("Teste")))
                .andExpect(jsonPath("$.valor", is(11.0)));

        verify(mockRepository, times(1)).save(any(Produto.class));

    }

    @Test
    public void atualizar_200() throws Exception {

        ProdutoDTO dto = new ProdutoDTO();
        dto.setNome("Teste");
        dto.setValor(11.0);

        Produto produto = new Produto();
        produto.setId(1);
        produto.setNome(dto.getNome());
        produto.setValor(dto.getValor());

        when(mockRepository.findById(1)).thenReturn(Optional.of(produto));
        when(mockRepository.save(any(Produto.class))).thenReturn(produto);

        mockMvc.perform(put(BASE_URL + "/1")
                .content(objectMapper.writeValueAsString(dto))
                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id", is(1)));
    }

    @Test
    public void deletar_200() throws Exception {

        Produto produto = new Produto();
        produto.setId(1);

        when(mockRepository.findById(1)).thenReturn(Optional.of(produto));

        mockMvc.perform(delete(BASE_URL + "/1"))
                .andExpect(status().isOk());

        verify(mockRepository, times(1)).deleteById(1);
    }
```

## Conclusão

Mostrei nesse post um simples exemplo de uma API REST com Spring, o código completo pode ser encontrado no Github: [https://github.com/pcollares/api-rest-spring](https://github.com/pcollares/api-rest-spring)

## Referências

Lista de links com referências para todos os assuntos abordados nesse post.

### Conceitos

- [https://restfulapi.net/resource-naming/](https://restfulapi.net/resource-naming/)
- [https://blog.caelum.com.br/rest-principios-e-boas-praticas/](https://blog.caelum.com.br/rest-principios-e-boas-praticas/)

### Spring

- [https://spring.io/guides/gs/rest-service/](https://spring.io/guides/gs/rest-service/)
- [https://blog.algaworks.com/como-criar-web-services-restful-com-spring-boot/](https://blog.algaworks.com/como-criar-web-services-restful-com-spring-boot/)
- [https://www.mkyong.com/tutorials/spring-boot-tutorials/](https://www.mkyong.com/tutorials/spring-boot-tutorials/)
- [https://blog.algaworks.com/injecao-de-dependencias-com-spring/](https://blog.algaworks.com/injecao-de-dependencias-com-spring/)

### Spring data JPA

- [https://www.mkyong.com/spring-boot/spring-boot-spring-data-jpa-postgresql/](https://www.mkyong.com/spring-boot/spring-boot-spring-data-jpa-postgresql/)
- [https://www.mkyong.com/spring-boot/spring-boot-spring-data-jpa/](https://www.mkyong.com/spring-boot/spring-boot-spring-data-jpa/)
- [https://domineospring.wordpress.com/2015/05/11/facilite-seus-daos-com-o-spring-data-jpa/](https://domineospring.wordpress.com/2015/05/11/facilite-seus-daos-com-o-spring-data-jpa/)

### MockMVC

- [http://marcelotozzi.com/teste/java/2014/09/10/usar-o-mockmvc-nos-testes-do-spring-e-mais-maneiro.html](http://marcelotozzi.com/teste/java/2014/09/10/usar-o-mockmvc-nos-testes-do-spring-e-mais-maneiro.html)
- [https://www.baeldung.com/integration-testing-in-spring](https://www.baeldung.com/integration-testing-in-spring)
- [https://howtodoinjava.com/spring-boot2/spring-boot-mockmvc-example/](https://howtodoinjava.com/spring-boot2/spring-boot-mockmvc-example/)
- [http://blog.marcnuri.com/mockmvc-spring-mvc-framework/](http://blog.marcnuri.com/mockmvc-spring-mvc-framework/)

### Swagger

- [http://www.marcelferry.com.br/tutoriais/documentando-suas-api-com-swagger-usando-springfox/](http://www.marcelferry.com.br/tutoriais/documentando-suas-api-com-swagger-usando-springfox/)
- [https://dzone.com/articles/swagger-generation-with-spring-boot](https://dzone.com/articles/swagger-generation-with-spring-boot)
- [https://www.baeldung.com/swagger-2-documentation-for-spring-rest-api](https://www.baeldung.com/swagger-2-documentation-for-spring-rest-api)

### DTO

- [https://www.baeldung.com/entity-to-and-from-dto-for-a-java-spring-application](https://www.baeldung.com/entity-to-and-from-dto-for-a-java-spring-application)
- [https://medium.com/@msealvial/blindando-sua-api-spring-boot-com-o-padr%C3%A3o-dto-44f97020d1a0](https://medium.com/@msealvial/blindando-sua-api-spring-boot-com-o-padr%C3%A3o-dto-44f97020d1a0)

### Paginação e ordenação

- [https://dzone.com/articles/pagination-and-sorting-with-spring-data-jpa](https://dzone.com/articles/pagination-and-sorting-with-spring-data-jpa)

[]’s