Alura > Cursos de Programação > Cursos de Java > Conteúdos de Java > Primeiras aulas do curso Spring Boot 3: documente, teste e prepare uma API para o deploy

Spring Boot 3: documente, teste e prepare uma API para o deploy

Agendamento de consultas - Apresentação

Boas-vindas ao terceiro curso de Spring Boot da Alura! Meu nome é Rodrigo Ferreira e continuarei acompanhando vocês neste curso, durante o qual exploraremos mais recursos do Spring Boot.

#acessibilidade Rodrigo Ferreira é uma pessoa de pele clara, com olhos castanhos e cabelos castanhos e curtos. Veste camiseta cinza e está sentado em uma cadeira de encosto preto. Ao fundo, há uma parede lisa com iluminação azul.

A ideia é continuarmos o mesmo projeto que desenvolvemos nos cursos anteriores, mas aprender a usar novos recursos e funcionalidades.

Nos cursos anteriores, aprendemos a:

O que aprenderemos neste curso

Um dos objetivos deste curso é implementarmos a funcionalidade de agendamento de consultas. Anteriormente, desenvolvemos o CRUD de médicos e o de pacientes. Faltou implementar a funcionalidade mais importante do projeto, o agendamento de consultas.

Também aprenderemos a documentar a nossa API para facilitar a vida de quem precisa consumi-la, ou seja, a equipe que desenvolverá o aplicativo mobile ou a aplicação Front-End da nossa API.

Essas pessoas precisam saber quais as URLs da nossa API, os métodos HTTP suportados, os parâmetros, o formato, o que é devolvido pela API e assim por diante. Geraremos uma documentação automática usando uma biblioteca que se integra com o Spring Boot.

Também aprenderemos a fazer testes automatizados em um projeto com Spring Boot. Já que implementamos repository e controllers, precisaremos fazer testes automatizados destes componentes.

Mas como escrever um teste com a biblioteca JUnit padrão do Java para testar essas classes e componentes que dependem de informações e recursos do Spring? Aprenderemos a fazer isso neste curso.

Por fim, aprenderemos sobre o build do projeto. Imagine que terminamos o nosso projeto e queremos fazer o seu deploy em algum servidor, seja um servidor Cloud ou interno da empresa.

Como geramos o pacote do projeto e fazemos o build? Como funciona o build de uma aplicação que utiliza o Spring Boot? Como executamos o projeto dentro de um servidor independentemente da plataforma utilizada? Também falaremos de tudo isso neste curso.

Além disso, continuaremos trabalhando no projeto da clínica médica, com mesmo quadro Trello com o detalhamento da funcionalidade e aprenderemos a usar esses recursos ao longo do curso.

Achou interessante? Eu também! Te espero na primeira aula!

Agendamento de consultas - Nova funcionalidade

Vamos começar o projeto! Estou com o Trello dele aberto no navegador. Na coluna "DONE", podemos observar que já implementamos as funcionalidades do CRUD do médico e de pacientes. À esquerda, na coluna "TO DO", faltaram as funcionalidades de agendamento e cancelamento de consultas.

Começaremos implementando o agendamento. Por isso, arrastaremos o cartão "Agendamento de Consultas" para a coluna "DOING". Com um clique sobre o cartão, abriremos as informações dele e, como de costume, leremos a descrição da funcionalidade para entender o que precisamos fazer.

A descrição diz o seguinte:

"O sistema deve possuir uma funcionalidade que permita o agendamento de consultas, na qual as seguintes informações deverão ser preenchidas: Paciente, Médico, Data/Hora da consulta".

"As seguintes regras de negócio devem ser validadas pelo sistema: (segue lista com regras de negócio)".

Essa é uma funcionalidade similar às que já implementamos. A nossa API receberá uma requisição com o ID de paciente ou de médico e a data em que a consulta será realizada.

Com essas informações, faremos as validações e as salvaremos no banco de dados. A diferença é que agora temos regras de negócio mais complexas do que as que vimos anteriormente para médico e paciente.

Abrirei o nosso layout do Figma com um modelo de aplicativo mobile para o nosso projeto.

Três telas com o layout de aplicativo mobile para agendar consultas médicas. Na primeira, da esquerda para a direita, há o cabeçalho "Consultas" no canto superior esquerdo, seguido do menu estilo kebab (três pontos alinhados na vertical) à direita. A primeira tela tem dois subtítulos principais: "Envolvidos" e "Data e horário". No primeiro subitem, há dois campos para preenchimento do usuário ("Nome do paciente" e "Nome do médico"). O segundo subitem permite a escolha da Data da Consulta e o seu Horário. A segunda tela apresenta o mesmo layout, agora com três dos quatro campos de preenchimento com o texto em vermelho e contendo a mensagem "Campo obrigatório" na parte inferior. A última tela mostra o layout com todos os campos preenchidos pela pessoa usuária. Os três layouts têm dois botões na parte inferior ("Agendar Consulta" e "Cancelar"), mas somente na última tela o botão "Agendar Consulta" pode ser selecionado, estando em destaque na cor azul.

No aplicativo mobile, a pessoa terá uma tela para agendar uma nova consulta, que contém o nome do ou da paciente e do médico ou médica. Essas informações serão puxadas do nosso banco de dados. Além disso, haverá dois campos para escolha do dia e do horário da consulta, bem como um botão para confirmar o agendamento.

Com isso, o aplicativo mobile precisa fazer validações dos campos obrigatórios, sendo opcional apenas o campo "Nome do médico".

Quando a pessoa preencher todos os campos obrigatórios e clicar em "Agendar consulta", uma requisição será disparada para a nossa API. Precisaremos receber esses dados e fazer o tratamento deles conforme já fizemos nas outras funcionalidades.

Voltando ao cartão no Trello, a única diferença são algumas das validações, que exigirão a escrita de um algortimo, consulta no banco de dados etc.

Assim, estas não são só validações de formulário como aquelas com as quais trabalhamos anteriormente com BIN validation (campo obrigatório, campo número ou texto, tamanho mínimo e máximo etc.). Agora, teremos que aprender a trabalhar com validações um pouco mais complexas.

Códigos para novas funcionalidades

Se pararmos para pensar, boa parte do código que usaremos para implementar essa funcionalidade será reaproveitado dos passos que fizemos anteriormente.

Assim, para implementar esta ou quaisquer outras funcionalidades, seguimos uma espécie de passo-a-passo. Precisamos criar sempre os seguintes tipos de códigos:

Estes são os cinco tipos de código que sempre desenvolveremos para uma nova funcionalidade. Isso também se aplica ao agendamento das consultas, incluindo um sexto item à lista, as regras de negócio. Nesta aula, entenderemos como implementar as regras de negócio com algoritmos mais complexos.

Implementando a funcionalidade

Agora que já entendemos o que precisa ser feito, desenvolveremos a funcionalidade por partes. Começaremos pelos cinco primeiros itens da lista, que são mais básicos. Em seguida, abordaremos a parte de regras de negócio.

Abrirei o IntelliJ com o projeto importado na IDE. É o mesmo projeto do curso anterior, que finalizamos com a parte de segurança e tratamento de erros.

Como a primeira parte seguirá um mesmo padrão, deixei essas classes prontas no código. Criei um novo ConsultaController no pacote "src > main > java > med.voll.api > controller".

Já que não estaremos mais cadastrando pacientes, mas consultas, a ideia é ter um Controller para receber essas requisições relacionadas a agendamento de consultas.

Ela é uma classe Controller, com as anotações do Spring: @RestController e @RequestMapping("consultas"). Ele mapeia requisições que chegam com a URI "/consultas", sabendo que deve chamar este controller e não os outros.

Em seguida, temos um método anotado com @PostMapping. Então, a requisição para agendar uma consulta será do tipo Post, assim como observamos nas outras funcionalidades.

Ele recebe um DTO com os dados do agendamento (DadosAgendamentoConsulta) e, no momento, a única coisa que fiz de diferente foi dar um System.out nos dados, para garantir que eles cheguem corretamente. Por fim, o método devolve um código 200 com o DTO de resposta.

Perceba que temos um DTO que representa os dados que chegam da API, ou seja, o nome da pessoa paciente, do médico ou médica e a data e hora da consulta (DadosAgendamentoConsulta).

// Trecho de código suprimido

@RestController
@RequestMapping("consultas")
public class ConsultaController {

    @PostMapping
    @Transactional
    public ResponseEntity agendar(@RequestBody @Valid DadosAgendamentoConsulta dados) {
        System.out.println(dados);
        return ResponseEntity.ok(new DadosDetalhamentoConsulta(null, null, null, null));
    }

}

Ao abrirmos o DadosAgendamentoConsulta, perceberemos que se trata de um record conforme os outros que vimos anteriormente. Ele tem os campos que chegam da API (Long idMedico, Long idPaciente e LocalDateTime data) e as anotações do BIN validation @NotNull para o ID da pessoa paciente e para a data, além de a data ter que ser no futuro (@Future), ou seja, não podemos agendar uma consulta para dias que já passaram.

// Trecho de código suprimido

public record DadosAgendamentoConsulta(
        Long idMedico,

        @NotNull
        Long idPaciente,

        @NotNull
        @Future
        LocalDateTime data,

        Especialidade especialidade) {
}

Voltando ao Controller, o nosso outro DTO é o de resposta, chamado DadosDetalhamentoConsulta. Ele devolve o ID da consulta criada, do médico, da pessoa paciente e a data da consulta cadastrada no sistema.

Além disso, no pacote "src > main > java > med.voll.api > domain", criamos o subpacote "consulta", que abrange as classes relacionadas ao domínio de consulta.

Dentre elas, temos a entidade JPA "Consulta.java", que contém as anotações da JPA e do Lombok, além das informações da consulta: médico, paciente e data.

// Trecho de código suprimido

@Table(name = "consultas")
@Entity(name = "Consulta")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Consulta {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "medico_id")
    private Medico medico;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "paciente_id")
    private Paciente paciente;

    private LocalDateTime data;

}

Neste caso, medico e paciente são relacionamentos com as outras entidades Medico e Paciente.

Também criamos o "ConsultaRepository.java", vazio por enquanto.

package med.voll.api.domain.consulta;

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

public interface ConsultaRepository extends JpaRepository<Consulta, Long> {

}

Por fim, temos a migration em "src > main > java > med.voll.api > resources", na pasta "db.migration". Tínhamos cinco migrations, mas agora criei a migration de número 6 ("V6"), que cria a tabela de consultas:

create table consultas(

    id bigint not null auto_increment,
    medico_id bigint not null,
    paciente_id bigint not null,
    data datetime not null,

    primary key(id),
    constraint fk_consultas_medico_id foreign key(medico_id) references medicos(id),
    constraint fk_consultas_paciente_id foreign key(paciente_id) references pacientes(id)

);

Nela, temos o ID da consulta, o ID de paciente, ID de médico e a data, sendo que medico_id e paciente_id são chaves estrangeiras que apontam para os IDs das tabelas de médico e paciente.

Estes são os códigos padrão para qualquer funcionalidade, com as suas respectivas mudanças de acordo com o projeto. Cada uma criará um controller ou uma entidade distinta, mas o funcionamento é o mesmo.

Eu já rodei o projeto e ele gerou um log na aba "Run", localizada no menu inferior do IntelliJ. Ele detectou a versão 6 da migration, executou esta última e criou a tabela de consultas.

O projeto foi inicializado sem nenhum erro. Já podemos tentar disparar uma requisição para esse endereço "/consultas" e verificar se ele cai no "ConsultaController.java" e nos dá o System.out exibindo os dados que chegaram no JSON da requisição.

Para disparar a requisição, continuaremos usando o Insomnia. Aberta a sua interface, clicaremos no botão com o símbolo "+", no painel à esquerda, escolheremos a opção "HTTP Request". Dando um duplo clique sobre a request, vamos renomea-la para "Agendar Consulta".

No painel central, trocaremos a requisição de GET para POST e o endereço será http://localhost:8080/consultas. Logo abaixo, na aba "Body", clicaremos na seta para baixo e escolheremos a opção "JSON".

Se observarmos o "ConsultaController" no IntelliJ, o método agendar recebe como parâmetro o DTO DadosAgendamentoConsulta, com os seguintes campos: ID do médico, ID de paciente e data. Precisamos mandar no JSON os campos que têm exatamente estes nomes.

Voltando ao Insomnia, escreveremos:

{
"idPaciente": 1,
"idMedico": 1,
"data": 
}

Perceba que o atributo data é representado pelo LocalDateTime, a classe de datas que entrou no Java 8. Como fazemos para enviar uma data na requisição do JSON de maneira que o Spring consiga criar o objeto LocalDateTime corretamente? Que máscara usamos para a data?

É preciso que a data fique entre aspas, como se fosse uma string e tenha o formato de data americano: AAAA-MM-DD. Para separar a data da hora, escrevemos a letra "T" maiúscula e a hora no formato HH:MM:SS. Os segundos são opcionais.

Assim, a requisição ficará da seguinte forma:

{
"idPaciente": 1,
"idMedico": 1,
"data": "2023-10-10T10:00"
}

Clicaremos no botão "Send" para disparar a requisição. Recebemos uma mensagem de erro "403 Forbidden". Isso aconteceu porque estamos usando o Spring Security e não levei o cabeçalho com o token.

Então, clicaremos em "Efetuar login" no menu lateral esquerdo e faremos o login primeiro. Dispararemos a requisição de login com o usuário que temos no nosso banco de dados, a Ana Souza.

Feito isso, recebemos um token de volta na aba Preview, no canto direito da tela. Vamos copiá-lo e voltar à requisição Agendar Consulta". Na aba "Auth" da janela central, clicaremos no ícone de seta para baixo e escolheremos a opção "Bearer Token". No campo "Token", colaremos o token copiado e clicaremos em "Send".

Recebemos o código "200 OK". Ele nos devolveu o DTO de resposta, mas todos os campos estão null, pois não salvamos nada no banco de dados.

Voltaremos ao IntelliJ e abriremos a aba "Run" no canto inferior da tela. Veremos que ele nos deu o System.out com as informações que inserimos no Insomnia:

DadosAgendamentoConsulta[idMedico=1, idPaciente=1, data=2023-10-10T10:00]

Conseguimos disparar a requisição para a nossa API e recebemos os dados do agendamento de consulta. A primeira parte foi concluída: já temos todo o código necessário para receber a requisição, ter a entidade, ter o repository e ter a tabela no banco de dados.

O nosso "ConsultaController.java", por enquanto, apenas dá um System.out. Podemos até apagá-lo porque agora começaremos a fazer as validações das regras de negócio. Mostrarei como implementá-las da maneira mais apropriada no próximo vídeo.

Agendamento de consultas - Classe Service

Já implementamos o esqueleto da funcionalidade. Agora, precisamos implementar as regras de negócio.

Antes, abriremos novamente o cartão do Trello que descreve a funcionalidade. Precisamos implementar agora uma série de validações com objetivos distintos.

O nosso trabalho será um pouco diferente do que já fizemos com a validação de campos de formulário via BIN validation. Agora, as validações são mais complexas. Mas como fazemos para implementá-las?

Voltando ao IntelliJ e observando o "ConsultaController.java", poderíamos fazer todas as validações no método agendar(), antes do retorno. No entanto, essa não é uma boa prática.

A classe controller não deve trazer as regras de negócio da aplicação.

Ela é apenas uma classe que controla o fluxo de execução: ao chegar uma requisição, ela chama a classe X, devolve a resposta Y. Se a condição for Z, ela devolve outra resposta e assim por diante. Ou seja, ela só controla o fluxo de execução e, por isso, não deveria ter regras de negócio.

Assim, isolaremos as regras de negócio, os algoritmos, os cálculos e as validações em outra classe que será chamada pelo Controller.

Expandiremos o menu "Project" na lateral esquerda da interface, selecionaremos o pacote "consulta" e usaremos o atalho "ALT + Insert", escolhendo em seguida a opção "Java Class". Com isso, criaremos uma classe para conter as regras de agendamento de consultas. Vamos chamá-la de "AgendaDeConsultas".

O nome é bem autoexplicativo: essa classe conterá a agenda de consultas. Podemos ter nesta classe outras funcionalidades ainda relacionadas ao agendamento de consultas.

Como acabamos de criar a classe e ela ainda não tem nenhuma anotação, o Spring Boot não consegue carregá-la automaticamente. Por isso, precisamos inserir alguma anotação primeiro.

No entanto, esta não é uma classe Controller tampouco uma classe de configurações. Esta classe representa um serviço da aplicação, o de agendamento de consultas. Por isso, será uma classe de serviços (Service) e levará a anotação @Service. O objetivo desta anotação é declarar o componente de serviço ao Spring Boot.

Dentro desta classe, criaremos um método public void agendar(), que recebe como parâmetro o DTO DadosAgendamentoConsulta.

package med.voll.api.domain.consulta;

import org.springframework.stereotype.Service;

@Service
public class AgendaDeConsultas {

    public void agendar(DadosAgendamentoConsulta dados) {

    }

}

A classe Service executa as regras de negócio e as validações da aplicação.

Precisaremos usar esta classe em "ConsultaController.java". Precisamos declarar um atributo do tipo AgendaDeConsultas, chamando-o de agenda. Para pedir ao Spring instanciar este objeto, usaremos o @Autowired acima do atributo.

// Trecho de código suprimido

@Autowired
private AgendaDeConsultas agenda;

// Trecho de código suprimido

Com isso, injetamos a classe AgendaDeConsultas no Controller. Já no método agendar do Controller, pegaremos o objeto agenda e chamaremos o método agendar(), passando como parâmetro os dados que chegam ao Controller. Tudo isso antes do retorno.

@PostMapping
@Transactional
public ResponseEntity agendar(@RequestBody @Valid DadosAgendamentoConsulta dados) {
    agenda.agendar(dados);
    return ResponseEntity.ok(new DadosDetalhamentoConsulta(null, null, null, null));
}

O Controller recebe as informações, faz apenas a validação do BIN validation e chama a classe Service AgendaDeConsultas, que executará as regras de negócio. Esta é a forma correta de lidar com as regras de negócio.

Agora, abriremos a classe AgendaDeConsultas e escreveremos todas as validações que constam no cartão do Trello dentro do método agendar().

No fim das contas, o nosso objetivo é salvar o agendamento no banco de dados: recebemos a requisição com os dados de agendamento e precisamos salvá-los na tabela de consultas.

Por isso, precisamos acessar o banco de dados e a tabela de consultas nesta classe. Assim, declararemos um atributo ConsultaRepository, chamando-o de consultaRepository.

Logo acima do atributo, usaremos a anotação @Autowired para que o Spring Boot injete este repository na nossa classe Service.

No fim do método agendar(), inseriremos consultaRepository.save() e passaremos um objeto do tipo consulta, a entidade JPA. Obviamente, só podemos chamar este método se todas as validações tiverem sido executadas conforme as regras de negócio.

@Service
public class AgendaDeConsultas {

    @Autowired
    private ConsultaRepository consultaRepository;

    public void agendar(DadosAgendamentoConsulta dados) {
        consultaRepository.save(consulta);
    }

}

Já chamamos o save, mas ele está dando um erro de compilação, porque a variável consulta ainda não foi criada. Por isso, precisaremos executar essa ação.

@Service
public class AgendaDeConsultas {

    @Autowired
    private ConsultaRepository consultaRepository;

    public void agendar(DadosAgendamentoConsulta dados) {
        var consulta = new Consulta();
        consultaRepository.save(consulta);
    }

}

A entidade Consulta está anotada com @AllArgsConstructor, do Lombok, que gera um construtor com todos os atributos. Podemos usar esse mesmo construtor no "AgendamentoDeConsultas". O primeiro parâmetro é o ID null, pois é o banco de dados que passará o ID. Já o segundo é medico, paciente e data. Esta última virá no DTO, por meio do parâmetro dados.

Acontece que medico e paciente não chegam na requisição, mas sim o ID do médico e o ID do paciente. Por isso, precisamos setar o objeto inteiro na entidade, e não apenas o ID.

Por isso, precisaremos carregar medico e paciente do banco de dados. Precisaremos injetar, então, mais dois Repositories na nossa Service: MedicoRepository e PacienteRepository.

No método agendar(), precisamos criar um objeto paciente também. Usaremos o pacienteRepository.findById() para buscar o objeto pelo ID, que está dentro do DTO dados.

Na requisição só vem o ID, mas precisamos carregar o objeto inteiro. Assim, usamos o Repository para carregar pelo ID do banco de dados. O médico seguirá a mesma dinâmica.

@Service
public class AgendaDeConsultas {

    @Autowired
    private ConsultaRepository consultaRepository;

    @Autowired
    private MedicoRepository medicoRepository;

    @Autowired
    private PacienteRepository pacienteRepository;

    public void agendar(DadosAgendamentoConsulta dados) {
        var paciente = pacienteRepository.findById(dados.idPaciente());
        var medico = medicoRepository.findById(dados.idMedico());
        var consulta = new Consulta(null, medico, paciente, dados.data());
        consultaRepository.save(consulta);
    }

}

Aparecerá um erro de compilação porque o método findById() não devolve a entidade, mas um Optional. Assim, no fim da linha, antes do ponto e vírgula, precisamos escrever .get() ao lado de findById(). Isso faz com que ele pegue a entidade carregada.

// Trecho de código suprimido

var paciente = pacienteRepository.findById(dados.idPaciente()).get();
var medico = medicoRepository.findById(dados.idMedico()).get();
var consulta = new Consulta(null, medico, paciente, dados.data());
consultaRepository.save(consulta);

O nosso método agendar() na classe Service fará o seguinte: pegamos o ID e carregamos o paciente e o médico do banco de dados; criamos uma entidade consulta passando o médico, o paciente e a data que vem no DTO; e salvamos isso no banco de dados.

Porém, antes disso, precisamos escrever o código para fazer todas as validações que fazem parte das regras de negócio.

O código das regras de negócio aparecerá antes do último trecho que implementamos neste vídeo. Na sequência, abordaremos como fazer as validações da melhor maneira possível.

Sobre o curso Spring Boot 3: documente, teste e prepare uma API para o deploy

O curso Spring Boot 3: documente, teste e prepare uma API para o deploy possui 210 minutos de vídeos, em um total de 46 atividades. Gostou? Conheça nossos outros cursos de Java em Programação, ou leia nossos artigos de Programação.

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