Alura > Cursos de Back-end > Cursos de Java > Conteúdos de Java > Primeiras aulas do curso Spring Boot: APIs REST profissionais com segurança e testes

Spring Boot: APIs REST profissionais com segurança e testes

Arquitetura REST mais madura - Apresentação

Apresentando a instrutora e contexto visual

Eu sou a Damiana Costa, sou principal engenheira no Itaú e também sou professora da FIAP. Tenho aproximadamente 20 anos na área de desenvolvimento. Atualmente atuo na área de arquitetura e no desenvolvimento de sistemas em Java.

Audiodescrição: Damiana é uma mulher negra, de cabelos presos e encaracolados. Veste uma camiseta azul e está no estúdio da Alura, com uma parede azul ao fundo.

Apresentando o conteúdo e objetivos do curso

O que vamos aprender neste curso?

Vamos explorar arquitetura em camadas, abordando as responsabilidades de services (serviços), controllers (controladores) e repositories (repositórios).

Estudaremos joins (junções) e paginação: veremos em detalhe inner join (junção interna) e left join (junção à esquerda), e como construir essas operações no banco de dados.

Trataremos de autenticação e autorização com Spring Security e JWT, compreendendo como construir uma aplicação mais segura ao lidar com informações sensíveis, como usuário e senha.

Realizaremos testes unitários e integrados, cobrindo toda a aplicação.

Abordaremos relacionamentos e como implementá-los utilizando, por exemplo, joins e paginação.

Também trabalharemos exceções e logs, incluindo a configuração e o uso de níveis de log: warning (aviso), info (informação) e error (erro).

Vamos tratar de autorizações por roles (papéis), definindo quem está utilizando o sistema e quem tem permissão de acesso. Esse tema está diretamente conectado ao Spring Security.

Por fim, vamos abordar Spring Profiles e Spring Boot Actuator para colocar a aplicação no ar. Quais parâmetros precisamos configurar? O que precisamos aprender? Vamos trabalhar esse contexto.

No nosso projeto Run e Cycle, implementaremos um back-end (camada de servidor) de uma rede social, com feeds (linhas do tempo), comentários e curtidas. Vamos estruturar o payload (corpo de dados) dessas informações dentro do projeto.

Listando os pré-requisitos e convidando para a próxima aula

Os pré-requisitos são:

Estou super animada para te ver na próxima aula. Vamos lá?

Arquitetura REST mais madura - Conhecendo e rodando e projeto

Apresentando o projeto base

Visão geral do projeto base

Vamos analisar o nosso projeto base, que utilizaremos ao longo do curso. Trata-se de um projeto em Spring, semelhante ao projeto do Pomodoro que desenvolvemos anteriormente.

Para evidenciar o ponto de partida, veja a classe principal do Spring Boot que inicializa a aplicação:

package br.com.alura.runnercircleapi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RunnerCircleApiApplication {

    public static void main(String[] args) {
        SpringApplication.run(RunnerCircleApiApplication.class, args);
    }

}

Detalhando a API de treinos e operações CRUD

API de treino (CRUD — criar, ler, atualizar e deletar)

Temos a API de treino, que implementa um conjunto de operações do tipo CRUD (criar, ler, atualizar e deletar). O modelo principal é o objeto treino, com campos como: tipo de treino, duração em minutos, distâncias, calorias, quantidade de batimentos, descrição e imagem para fazermos o upload (envio).

Para visualizar a estrutura do modelo, observe a Entity que representa o treino no banco de dados:

package br.com.alura.runnercircleapi;

import ...

@Entity
@Table(name = "treinos")
public class Treino {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(EnumType.STRING)
    private TipoTreino tipoTreino;

    private Integer tempoEmMinutos;

    private Integer distanciaMetros;

    private Integer calorias;

    private Integer batimentos;

    private String descricao;

    private String imagemUrl;

    public Treino() {
    }
...

No Controller, as requisições que vêm do front-end (camada de interface) chegam à nossa API e são processadas. Nesse contexto:

Abaixo está o endpoint de busca por id, retornando um ResponseEntity com o DTO de resposta:

@GetMapping("/{id}")
@Operation(summary = "Busca um treino pelo id")
public ResponseEntity<TreinoResponseDTO> buscarPorId(@PathVariable Long id) {
    return treinoRepository.findById(id)
            .map(treino -> ResponseEntity.ok(toDto(treino)))
            .orElse(ResponseEntity.notFound().build());
}

O endpoint de criação recebe um TreinoRequestDTO, salva a entidade e retorna 201 Created com o TreinoResponseDTO:

@PostMapping
@Operation(summary = "Cria um novo treino")
public ResponseEntity<TreinoResponseDTO> criar(@Valid @RequestBody TreinoRequestDTO dto) {
    Treino treino = toEntity(dto);
    treino = treinoRepository.save(treino);
    return ResponseEntity.status(HttpStatus.CREATED).body(toDto(treino));
}

O endpoint de atualização localiza a entidade pelo id, aplica os dados do DTO e retorna o DTO atualizado:

@PutMapping("/{id}")
@Operation(summary = "Atualiza um treino existente")
public ResponseEntity<TreinoResponseDTO> atualizar(@PathVariable Long id, @Valid @RequestBody TreinoRequestDTO dto) {
    return treinoRepository.findById(id)
            .map(treino -> {
                treino.setTipoTreino(dto.tipoTreino());
                treino.setTempoEmMinutos(dto.tempoEmMinutos());
                treino.setDistanciaMetros(dto.distanciaMetros());
                treino.setCalorias(dto.calorias());
                treino.setBatimentos(dto.batimentos());
                treino.setDescricao(dto.descricao());
                treino = treinoRepository.save(treino);
                return ResponseEntity.ok(toDto(treino));
            })
            .orElse(ResponseEntity.notFound().build());
}

O endpoint de remoção verifica a existência, deleta e retorna 204 No Content:

@DeleteMapping("/{id}")
@Operation(summary = "Remove um treino")
public ResponseEntity<Void> remover(@PathVariable Long id) {
    if (!treinoRepository.existsById(id)) {
        return ResponseEntity.notFound().build();
    }
    treinoRepository.deleteById(id);
    return ResponseEntity.noContent().build();
}

Vamos, portanto, recapitular o CRUD (criar, ler, atualizar e deletar) por meio desses verbos HTTP.

Explicando DTOs, Entity e mapeamentos

DTOs, Entity e mapeamento

Falamos bastante no curso anterior sobre DTO (Objeto de Transferência de Dados) e sua importância no mapeamento entre a classe Java que escrevemos e o que exibimos para a pessoa usuária, bem como o que ela envia (por exemplo, via Postman ou Swagger) para a nossa API. Utilizamos:

Essas operações e mapeamentos são orquestrados pelo Controller.

No código atual, o próprio controller realiza o mapeamento entre Entity e DTO por meio de métodos auxiliares. Isso funciona, mas, como veremos mais à frente, não é a melhor prática de organização:

private TreinoResponseDTO toDto(Treino treino) {
    return new TreinoResponseDTO(
            treino.getId(),
            treino.getTipoTreino(),
            treino.getTempoEmMinutos(),
            treino.getDistanciaMetros(),
            treino.getCalorias(),
            treino.getBatimentos(),
            treino.getDescricao(),
            treino.getImagemUrl(),
            treino.getDataCriacao()
    );
}
private Treino toEntity(TreinoRequestDTO dto) {
    Treino treino = new Treino();
    treino.setTipoTreino(dto.tipoTreino());
    treino.setTempoEmMinutos(dto.tempoEmMinutos());
    treino.setDistanciaMetros(dto.distanciaMetros());
    treino.setCalorias(dto.calorias());
    treino.setBatimentos(dto.batimentos());
    treino.setDescricao(dto.descricao());
    return treino;
}

Definindo o enum de tipos e destacando a classe principal

Enum e classe principal

Já temos um enum com duas informações fixas, recurso que utilizamos com frequência para constantes de domínio. Também contamos com a classe principal do Spring Boot, responsável por iniciar a aplicação.

Aqui está o enum TipoTreino com as opções citadas:

package br.com.alura.runnercircleapi;

public enum TipoTreino {
    CAMINHADA,
    CORRIDA
}

(A classe principal já foi mostrada no início desta transcrição para contextualizar a execução do projeto.)

Integrando com o banco via repository e JPA

Repository e JPA Repository

O Repository (interface) integra a aplicação com o banco de dados. Em projetos de CRUD, o JpaRepository já fornece diversos métodos prontos para operações na base de dados, como salvar, atualizar, editar e deletar. Dessa forma, não precisamos implementá-los manualmente.

Veja a interface do repositório de Treino:

package br.com.alura.runnercircleapi;

import org.springframework.data.jpa.repository.JpaRepository;

public interface TreinoRepository extends JpaRepository<Treino, Long> {
}

Apresentando o modelo user e seu repositório

Modelo User e UserRepository

Já temos a classe User preparada, que utilizaremos no projeto. Ainda não há relação entre User e treino. O User inclui os campos id, nome, username, email, senha, biografia e avatar (URL da foto). Há um construtor que recebe essas informações e métodos get e set. Também temos o UserRepository, que funciona da mesma forma, oferecendo métodos de salvar, editar e excluir via JpaRepository.

A seguir, a entidade User (observe os campos e a estrutura base):

package br.com.alura.runnercircleapi;

import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String nome;
    private String username;
    private String email;
    private String senha;
    private String bio;
    private String avatarUrl;

    public User() {
    }
...

E o repositório correspondente:

package br.com.alura.runnercircleapi;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

Centralizando o tratamento global de exceções

Tratamento global de exceções

Contamos com uma classe de tratamento global de exceções (Global Exception Handler), responsável por centralizar as exceções que a API pode lançar. À medida que avançarmos no desenvolvimento ao longo do curso, essa classe será expandida.

O handler abaixo captura erros de validação e responde com 400 Bad Request, retornando um mapa de campos e mensagens:

package br.com.alura.runnercircleapi;

import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.LinkedHashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new LinkedHashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return errors;
    }
}

Configurando a aplicação e infraestrutura

Configurações e infraestrutura

No arquivo application.properties, definimos as configurações da base de dados e do Swagger. Além disso, o projeto já inclui configuração de Docker, deixando o ambiente completo para uso.

Confira as propriedades principais da aplicação:

spring.application.name=runner-circle-api

spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/runnercircle
spring.datasource.username=${DB_USER:postgres}
spring.datasource.password=${DB_PASSWORD:postgres}

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

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

E o docker-compose para subir o PostgreSQL localmente:

version: '3.8'
services:
  db:
    image: postgres:15
    container_name: runnercircle_db
    environment:
      POSTGRES_DB: runnercircle
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - "5432:5432"
    volumes:
      - runnercircle_db:/var/lib/postgresql/data

volumes:
  runnercircle_db:

Executando a aplicação e testando no Swagger

Execução

Vamos executar a classe principal para observar o comportamento da aplicação. O serviço está iniciando. Perfeito: a aplicação subiu.

Este é o Swagger. Não vamos usar o Postman para teste. No curso anterior, discutimos a importância do Swagger e, neste curso, utilizaremos o Swagger para realizar os testes.

Aqui, a aplicação disponibilizou somente o CRUD (Create, Read, Update, Delete) do treino, que é o que importa para nós, pois corresponde à nossa classe base. Temos o método GET para treinos (busca de um treino por ID, por exemplo), o PUT, o DELETE, outro GET que retorna uma lista com todos os treinos da base de dados e o POST.

Para completar o panorama das rotas, segue o GET responsável por listar os treinos (com um filtro opcional por tipo):

public class TreinoController {

    @Autowired
    private TreinoRepository treinoRepository;

    @GetMapping
    @Operation(summary = "Lista os treinos, com filtro opcional por tipo (CAMINHADA ou CORRIDA)")
    public List<TreinoResponseDTO> listar(@RequestParam(required = false) TipoTreino tipoTreino) {
        List<Treino> treinos = treinoRepository.findAll();

        if (tipoTreino != null) {
            return treinos.stream()
                    .filter(treino -> treino.getTipoTreino() == tipoTreino)
                    .map(this::toDto)
                    .collect(Collectors.toList());
        }

        return treinos.stream()
                .map(this::toDto)
                .collect(Collectors.toList());
    }
...

Executando o GET, percebemos que não há registros; está vazio porque ainda não existe nenhum treino. Vamos criar um treino para termos algo na base e iniciarmos os testes.

Ao criar um treino novo, este é o body (corpo) que precisamos enviar na requisição: o tipo de treino (caminhada ou corrida; lembrando do enum apresentado), o tempo em minutos, a distância, as calorias, os batimentos e a descrição do treino. Vamos deixar como caminhada, com 10 minutos, distância em metros (10 metros), calorias 90, batimentos 120 e descrição “Esta é uma boa caminhada”.

Primeiro, o Swagger pode sugerir um modelo base como este (apenas para referência):

{
  "tipoTreino": "CAMINHADA",
  "tempoEmMinutos": 0,
  "distanciaMetros": 0,
  "calorias": 0,
  "batimentos": 0,
  "descricao": "string"
}

Agora, vamos enviar os valores definidos para o nosso primeiro exemplo de treino (caminhada):

{
  "tipoTreino": "CAMINHADA",
  "tempoEmMinutos": 10,
  "distanciaMetros": 10,
  "calorias": 90,
  "batimentos": 120,
  "descricao": "Essa é uma boa caminhada."
}

Vamos executar. Se estiver tudo correto, receberemos um 201, que é um status code (código de status) indicando que o recurso foi criado. É o mesmo que aconteceria se executássemos pelo Postman, mas nós preferimos utilizar o Swagger por ser mais prático.

O que foi retornado? Recebemos 201, indicando que o recurso de treino foi criado. No response (resposta), já vemos o ID 1, do tipo caminhada, com as informações preenchidas. Observamos que o campo de imagem, a URL, está nulo porque ainda não chegamos na etapa de fazer o upload (envio de arquivos); isso será tratado em uma próxima aula. Também são exibidos os headers (cabeçalhos) enviados: é uma requisição do tipo JSON, com a hora exata (GMT) e outras informações importantes da request (requisição).

Vamos criar mais um registro para manter a organização. Agora, vamos criar uma corrida. Informamos 20 no tempo e mantivemos o restante. Descrição: “Corrida no parque”, como exemplo. Executando, se estiver tudo certo, é exibido o ID 2, com a data de criação e todas as informações preenchidas.

O corpo para essa segunda criação (corrida) fica assim:

{
  "tipoTreino": "CORRIDA",
  "tempoEmMinutos": 20,
  "distanciaMetros": 10,
  "calorias": 90,
  "batimentos": 120,
  "descricao": "Corrida no parque."
}

Vamos conferir como ficou. Ao executar um GET, vemos que agora existem dois recursos (dois registros) na base. Assim, confirmamos que a nossa API base está funcionando.

Analisando responsabilidades do controller e propondo ajustes

Agora, vamos voltar ao código e analisar novamente o controller. Precisamos nos atentar à responsabilidade das classes. É uma boa prática estruturar as classes de acordo com suas responsabilidades. O controller (controlador) é a classe que faz a ligação entre a API (que recebe as informações) e o código Java (classe de serviços e demais componentes).

Qual é a responsabilidade específica do controller? Trabalhar as rotas. Por exemplo, no GET, manter as informações de request adequadas faz sentido, porque, sempre que formos buscar um ID, receberemos esse ID na requisição; a pessoa usuária é quem fornece esse ID. Tudo que for relacionado a verbos HTTP e aos dados da requisição é responsabilidade do controller.

Entretanto, há um problema neste controller: existem informações que não deveriam estar aqui, como o DTO. O DTO, que representa um mapeamento de dados, não é responsabilidade do controller; isso é responsabilidade, por exemplo, de uma classe de serviço (service). A parte da Entity (entidade), que se relaciona ao DTO, também não deveria estar no controller. De modo geral, o DTO não deveria aparecer aqui.

Esse problema fica claro ao notarmos que o controller está realizando conversões com toDto e toEntity (mostradas acima). Ao acumular esse tipo de lógica no controller, aumentamos o acoplamento e tiramos o foco da responsabilidade principal (as rotas e a orquestração da requisição/resposta).

Outro ponto importante sobre responsabilidade: o controller deveria ser uma classe apenas de request, e nada mais. Isso se torna um problema porque, conforme o projeto cresce, passamos a colocar mais elementos dentro do controller sem necessidade. É fundamental separar essas responsabilidades.

Portanto, ao analisar o nosso controller, identificamos uma má prática de mercado que precisamos corrigir. Quanto mais informação colocamos dentro do controller que não faz parte da sua responsabilidade, mais responsabilidade compartilhada criamos, aumentando o acoplamento e dificultando a manutenção.

Para resolver esse problema, vamos ajustar o código seguindo as boas práticas.

Eu vejo você no próximo vídeo, para nós podermos resolver esse problema.

Arquitetura REST mais madura - Criando a classe service

Apresentando a camada service e separando responsabilidades

Na aula passada, falamos sobre o problema de concentrar todas as responsabilidades no Controller. Hoje, vamos criar uma camada de Service. Antes, tínhamos o Controller, que chamava o Repository para, por exemplo, criar um treino. O Controller invocava o método que salvava o treino na base de dados e, em seguida, chamava o banco.

Agora, vamos trabalhar com a separação de responsabilidades. Teremos um Controller para gerenciar o tráfego das requisições HTTP, definir quais são os endpoints que vamos chamar, quais são os métodos, quais são os parâmetros que vamos passar e o que vamos devolver. E teremos a classe Service, responsável por implementar as regras de negócio. Assim, o Controller chamará o Service, e o Service será responsável por fazer a chamada ao Repository.

Por exemplo, ao criar um novo treino, chamamos o endpoint POST e enviamos todas as informações necessárias para criar o treino. O Controller fará a injeção de dependência na classe Service, e o Service orquestrará as operações na base de dados por meio do Repository. Se precisarmos editar, excluir, criar um recurso novo ou simplesmente realizar um GET para trazer tudo o que existe na base de dados, essa responsabilidade deve ser do Service, e não mais do Controller. A responsabilidade do Controller é trabalhar apenas com a parte de HTTP. Já a responsabilidade do Service é lidar com as regras de negócio e definir quais operações serão executadas. Portanto, salvar um novo treino, editar um treino, excluir um treino ou listar treinos é responsabilidade da classe Service. O Repository tem a responsabilidade de persistir essas operações na base de dados.

Solicitando à ia a criação do service e refatorando o controller

Para viabilizar essa separação, vamos solicitar ao agente de IA que faça o ajuste para nós. Pediremos a criação da classe Service, extraindo as regras de negócio que estão dentro do Controller (por exemplo, o DTO) e organizando a estrutura. Para isso, é importante contextualizarmos o agente de IA que vamos usar. Nós, por exemplo, estamos usando o Claude. Independentemente do agente de IA utilizado, é essencial contextualizá-lo sobre o projeto e sobre o que estamos fazendo. Se isso ainda não foi feito, é necessário fazer. Com o contexto preparado, vamos pedir para a IA gerar o Service, extrair as informações do Controller e deixar cada componente com suas respectivas responsabilidades.

A IA perguntou se poderia criar o arquivo de Service, o TreinoService, dentro do projeto. Confirmamos a criação. É sempre importante lermos atentamente o que a IA solicita, pois ela pode propor algo que não pedimos; por isso, precisamos revisar. Ela solicitou atualizar o nosso Controller; confirmamos para que pudesse modificar o serviço como um todo.

Em seguida, a IA modificou e refatorou o Controller, identificando o que poderia ser extraído, como o método listar. Ela analisou o projeto, mencionou o Maven e outros detalhes. A compilação ocorreu sem problemas.

Para visualizar essa refatoração no Controller, observe como o endpoint de listagem ficou mais enxuto, delegando a lógica de negócio para o Service e mantendo no Controller apenas o mapeamento HTTP e a conversão para DTO:

@RestController
@RequestMapping("/treinos")
@Tag(name = "Treinos", description = "CRUD de Treinos (corrida e caminhada) do Runner Circle")
public class TreinoController {

    @Autowired
    private TreinoService treinoService;

    @GetMapping
    @Operation(summary = "Lista os treinos, com filtro opcional por tipo (CAMINHADA ou CORRIDA)")
    public List<TreinoResponseDTO> listar(@RequestParam(required = false) TipoTreino tipoTreino) {
        return treinoService.listar(tipoTreino).stream()
                .map(this::toDTO)
                .collect(Collectors.toList());
    }
}

Efetivamente, foram alterados dois arquivos. O arquivo TreinoController foi ajustado para remover responsabilidades indevidas, que passaram para a classe TreinoService. A classe TreinoService foi criada e concentrou as regras de negócio extraídas do Controller. Ao atualizar o diretório, o que está em azul indica o que foi modificado e o que está em vermelho indica arquivos novos criados.

Ao verificar o Controller, observamos que a classe ficou mais enxuta e simples, com responsabilidade apenas por request, response e pela definição dos endpoints. O DTO ainda está no Controller, mas vamos ajustá-lo no próximo vídeo. Em seguida, vamos analisar o conteúdo da classe TreinoService.

Antes, ainda no Controller, além da listagem, ficaram os endpoints que exemplificam bem o uso correto de verbos HTTP e códigos de status. O GET /treinos/{id} retorna 200 OK quando encontra o recurso e 404 Not Found quando não existe; já o POST /treinos retorna 201 Created ao criar um novo treino:

@GetMapping("/{id}")
@Operation(summary = "Busca um treino pelo id")
public ResponseEntity<TreinoResponseDTO> buscarPorId(@PathVariable Long id) {
    return treinoService.buscarPorId(id)
            .map(this::toDTO)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
}

@PostMapping
@Operation(summary = "Cria um novo treino")
public ResponseEntity<TreinoResponseDTO> criar(@RequestBody TreinoRequestDTO dto) {
    Treino treino = treinoService.criar(dto);
    return ResponseEntity.status(HttpStatus.CREATED).body(toDTO(treino));
}

Detalhando a extração para o service e seus métodos

O que foi extraído? Tudo que está relacionado à base de dados. O método listar, por exemplo, é o método que acessa a base de dados e lista todos os treinos. Essa responsabilidade foi removida da classe Controller (controlador) e transferida para a classe Service (serviço). O método criar, para criar um novo treino, também foi removido da classe Controller e mantido dentro da Service, pois essa é, de fato, a responsabilidade de um serviço. Aqui poderíamos realizar validações, por exemplo: onde foi feito esse treino, que tipo de treino é, e quaisquer regras de negócio necessárias. É nesse ponto que faríamos validações com if e else, entre outras. O mesmo vale para remover/deletar um treino quando não quisermos mais mantê-lo. Essas responsabilidades agora são atribuídas à classe Service, e não mais à classe Controller.

Para refletir essa extração, veja como a classe TreinoService centraliza as operações de acesso e manipulação de dados. Começando pelo método de listagem, que aplica um filtro opcional por TipoTreino:

@Service
public class TreinoService {

    @Autowired
    private TreinoRepository treinoRepository;

    public List<Treino> listar(TipoTreino tipoTreino) {
        List<Treino> treinos = treinoRepository.findAll();

        if (tipoTreino != null) {
            treinos = treinos.stream()
                    .filter(treino -> treino.getTipoTreino() == tipoTreino)
                    .collect(Collectors.toList());
        }

        return treinos;
    }
}

Em seguida, o método de criação passa a construir a entidade a partir do TreinoRequestDTO, permitindo que futuras validações e regras de negócio ocorram nesta camada:

public Treino criar(TreinoRequestDTO dto) {
    Treino treino = new Treino();
    treino.setTipoTreino(dto.tipoTreino());
    treino.setTempoEmMinutos(dto.tempoEmMinutos());
    treino.setDistanciaMetros(dto.distanciaMetros());
    treino.setCalorias(dto.calorias());
    treino.setDescricao(dto.descricao());
    treino.setData(dto.data());
    return treinoRepository.save(treino);
}

Por fim, a remoção também fica sob responsabilidade do Service, que decide o que fazer quando o recurso não existe e orquestra a persistência:

public boolean remover(Long id) {
    if (treinoRepository.existsById(id)) {
        treinoRepository.deleteById(id);
        return true;
    }
    return false;
}

Validando a refatoração e evoluindo a maturidade da api

A IA já estruturou tudo para nós. Vamos executar para verificar se nada quebrou e se tudo está funcionando corretamente. Com a aplicação em execução, vamos conferir se o Swagger (ferramenta de documentação) está funcionando adequadamente. No Swagger, acessamos o método getTrainings. No botão Try it out (experimentar), vamos executar a chamada clicando em Execute (executar). A resposta retornará todos os treinos disponíveis. Até o momento, temos dois treinos na nossa base de dados.

Essa é a forma de evoluir nossa aplicação e nosso serviço. A ideia é evoluirmos nossa API em um processo de maturidade. Nesse sentido, trazemos a Maturidade de Richardson, que é importante para o nível de API REST com o qual estamos trabalhando.

No curso passado, estávamos no nível 0, pois tínhamos um único endpoint (ponto de extremidade), o do Pomodoro. Nele, realizávamos POST, GET, DELETE, entre outros. Quando mantemos um único endpoint com uma finalidade específica, estamos no nível 0 da maturidade.

Agora começamos a evoluir para o nível 1 de maturidade, no qual trabalhamos a parte de recursos. Nesse nível, identificamos recursos como treino, usuário, comentário — tudo que se relaciona a uma entidade, tratado como recurso.

No nível 2 da Maturidade de Richardson, utilizamos o recurso combinado com o verbo HTTP e o status code (código de status). Exemplos:

Esses códigos de status são globais, usados de forma consistente, e nos ajudam a identificar como a nossa API está funcionando. Esse é o nível em que estamos hoje.

Vamos criar, além do recurso de treino, outras APIs e seguiremos exatamente o nível 2, utilizando recursos, verbos HTTP e códigos de status — isto é, POST, GET, DELETE e PATCH. Também falamos bastante sobre isso no primeiro curso, que serviu como base para entendermos as APIs que estamos desenvolvendo aqui.

Para contextualizar, o nível 3 é denominado HATEOAS (hipermídia como motor do estado da aplicação). Nesse nível, as respostas trazem links para as próximas ações. Por exemplo, após um cadastro, a resposta pode incluir links para editar, deletar ou navegar para outra página. O HATEOAS possibilita essa navegação dirigida por hipermídia. No nosso curso, não vamos trabalhar com HATEOAS (nível 3), pois é um nível mais complexo.

Quando entendemos que estamos no nível 2 do Modelo de Maturidade de Richardson, significa que a nossa API já é madura, de nível profissional. Nosso objetivo, ao longo do curso, é avançar continuamente para mantermos um serviço e uma API maduros em nível de mercado.

Eu vejo você no próximo vídeo.

Sobre o curso Spring Boot: APIs REST profissionais com segurança e testes

O curso Spring Boot: APIs REST profissionais com segurança e testes possui 349 minutos de vídeos, em um total de 76 atividades. Gostou? Conheça nossos outros cursos de Java em Back-end, ou leia nossos artigos de Back-end.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Java acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas