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.
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.
Os pré-requisitos são:
Estou super animada para te ver na próxima aula. Vamos lá?
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);
}
}
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:
GET por id: recebemos as informações de um treino a partir do id do treino.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());
}
POST: criamos um novo treino com as informações fornecidas.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));
}
PUT: atualizamos dados de um treino existente, como nome e descrição.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());
}
DELETE: removemos um treino (por exemplo, quando estiver obsoleto), a partir do id.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.
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:
DTO de entrada (Request): o que enviamos para a API.DTO de saída (Response): o que esperamos como resposta.Entity (entidade): representação do objeto persistido no banco de dados.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;
}
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.)
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> {
}
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> {
}
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;
}
}
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:
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.
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.
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.
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));
}
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;
}
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:
200. Há códigos de status específicos para essas ações, e um deles é o 201 Created, que é um padrão global. Assim, sempre que um recurso for criado, retornamos 201.GET em uma lista de treinos, ou buscar por um ID específico de treino que não foi encontrado, devemos retornar 404 Not Found (não encontrado).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.
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:
O Plano Plus evoluiu: agora com Luri para impulsionar sua carreira com os melhores cursos e acesso à maior comunidade tech.
2 anos de Alura
Matricule-se no plano PLUS 24 e garanta:
Jornada de estudos progressiva que te guia desde os fundamentos até a atuação prática. Você acompanha sua evolução, entende os próximos passos e se aprofunda nos conteúdos com quem é referência no mercado.
Back-end, Dados, Front-end, DevOps, Mobile, Gestão & Negócios, UX & Design, Cibersegurança, Cloud, Inteligência Artificial
Formações com mais de 1500 cursos atualizados e novos lançamentos semanais, em Programação, Inteligência Artificial, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.
A cada curso ou formação concluído, um novo certificado para turbinar seu currículo e LinkedIn.
Acesso à inteligência artificial da Alura.
No Discord, você participa de eventos exclusivos, pode tirar dúvidas em estudos colaborativos e ainda conta com mentorias em grupo com especialistas de diversas áreas.
Catálogo de tecnologia para quem é da área de Marketing
Faça parte da maior comunidade Dev do país e crie conexões com mais de 120 mil pessoas no Discord.
Acesso ilimitado ao catálogo de Imersões da Alura para praticar conhecimentos em diferentes áreas.
Explore um universo de possibilidades na palma da sua mão. Baixe as aulas para assistir offline, onde e quando quiser.
20% de desconto na Pós Tech
Luri Vision chegou no Plano Pro: a IA da Alura que enxerga suas dúvidas, acelera seu aprendizado e conta também com o Alura Língua que prepara você para competir no mercado internacional.
2 anos de Alura
Todos os benefícios do PLUS 24 e mais vantagens exclusivas:
Acesso ao catálogo da Casa do Código e leitura dentro da plataforma
Chat, busca, exercícios abertos, revisão de aula, geração de legenda para certificado.
Modo entrevista - Pratique situações reais e evolua com feedback personalizado
Envie imagens para a Luri e ela te ajuda a solucionar problemas, identificar erros, esclarecer gráficos, analisar design e muito mais.
Aprenda um novo idioma e expanda seus horizontes profissionais. Cursos de Inglês, Espanhol e Inglês para Devs, 100% focado em tecnologia.
Para quem quer atingir seus objetivos mais rápido: Luri Vision ilimitado, vagas de emprego exclusivas e mentorias para acelerar cada etapa da jornada.
2 anos de Alura
Todos os benefícios do PRO 24 e mais vantagens exclusivas:
Lives CareerUp: eventos exclusivos com foco em empregabilidade e carreira
Mentorias de carreira: 2 encontros individuais com mentores do Talent Lab
Conecte-se ao mercado com mentoria individual personalizada, vagas exclusivas e networking estratégico que impulsionam sua carreira tech para o próximo nível.