1

API REST de Cadastro de Dados Pessoais

#Java #Spring Web MVC #JPA
Edson Ajeje
Edson Ajeje

Vamos construir uma API REST com um endpoint para manter um cliente. Para isso, vamos utilizar a stack Java e Spring + Hibernate.

O primeiro passo é ir ao site Spring Initializr onde é possível montar um boot para iniciar o projeto spring. Primeiro escolhemos o gerenciador de dependências para o projeto, a linguagem e qual versão do spring boot utilizaremos. O site oferece Maven e Gradle para gerenciador, Java, Kotlin e Groovy para linguagem de desenvolvimento e as versões 2.3.7 até 2.5.0 do spring boot. Para o nosso projeto selecionamos Maven, Java e 2.4.1.


Com a versão do Spring Boot escolhida precisamos escolher as dependências que serão instaladas no projeto. Ao clicar no botão “Add Dependencies” um popup se abre para escolha das dependências. Para selecionar mais de uma, basta segurar a tecla “ctrl” do teclado e clicar sobre as dependências selecionadas. Como dependências para o projeto escolhemos o Spring Boot DevTools, que oferece vários recursos para melhorar a experiencia de desenvolvimento, o Spring Web para facilitar a criação de web applications como API REST, Spring MVC. Por fim, Spring Data JPA e PostgreSQL Driver para fazer a comunicação com o banco e utilizar o ORM Hibernate.

O próximo passo é preencher as informações de metadata escolher o packaging e a versão do Java. Escolhemos jar e java 11. E ao clicar no botão “Generate”, inicia o download do boot do projeto. Tudo muito simples.


Começando a Programar: Preparando os Pacotes


Com o projeto pronto e aberto na IDE ou editor de código de sua preferência, aqui vou utilizar a IDE da Jetbrains Intellij Idea, vamos começar a codificação do projeto.

O inicio é importante. Trata, em um padrão MVC, de cuidar do model, o coração da API. Então iniciamos criando, dentro do pacote principal, o pacote model. Ele será responsável por nossas entidades e o repositório, que é construído pensando no padrão de projetos repositor.

Dentro do pacote model vamos criar o pacote entities com as classes Client e ClientResponse. Já veremos porque a criação dessas duas classes. E criaremos o pacote repository e dentro a interface ClientRepository.

A classe Client será responsável por nosso modelo de banco de dados e como estamos utilizando o Spring e JPA + Hibernate, utilizando algumas annotations podemos delegar a criação das querys e manutenção do banco de dados para o spring.

Para que o spring possa fazer esse gerenciamento automatizado, precisamos realizar uma configuração dentro da pasta resources, no arquivo application.properties. Nessa configuração vamos primeiro configurar o banco, driver de conexão e a url de conexão. Colocamos também o usuário e a senha para acesso ao banco. Depois configuramos o modo de operação do hibernate para update, assim ele sempre atualiza o banco sem realizar um DROP e a criação de novas tabelas na inicialização. Configuramos, por ultimo o dialeto. Assim o hibernate sabe qual linguagem SQL ele vai utilizar nas querys.


spring.datasource.url=jdbc:postgresql://localhost:5432/db_bank
spring.datasource.username=postgres
spring.datasource.password=root

spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect


Client e ClientResponse


Voltando a classe Client, criamos uma classe pública padrão do java, sem nada demais. Atributos privados, construtores e métodos getters e setters. A diferença está na presença de annotations. Para indicar para o Hibernate e para o Spring que eles precisam criar uma tabela no banco para essa classe, antes da definição da classe utilizamos o @Entity. Ele garante esse gerenciamento automatizado.

Como requisito do projeto, foi pedido que o CPF e o e-mail fossem valores únicos, ou seja, sem possibilidade de cadastro de dois clientes com o mesmo CPF ou com e-mails repetidos. Para garantir isso utilizamos a annotation @Table, que recebe o argumento uniqueConstraints com o nome do campo unico como valor.

O Hibernate também ajuda com a validação dos campos de CPF e e-mail. Marcando essas propriedades com os annotations @CPF e @EMAIL, respectivamente, as validações para verificar se o CPF e o e-mail são validos ficam preparadas. Essas validações são transparentes para nós, mas caso você queira saber mais sobre elas, basta buscar no google por Hibernate e @CPF, por exemplo, que fica fácil achar a documentação oficial e verificar essas validações.

Como o CPF é um campo único, poderia ser uma estratégia utilizar esse campo para chave primária da tabela. Porém gosto de utilizar uma chave primária incremental e deixar o banco cuidar disso. Assim marquei o atributo id da classe com os annotations @Id e @GeneratedValue, com a estratégia de geração IDENTITY. Assim o banco se responsabiliza pela criação de uma chave primária. Segue o código da classe Client, contendo os atributos, construtores, getters e setters.


package br.com.edsonajeje.cadatroAPI.model.entities;

import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.br.CPF;

import javax.persistence.*;
import javax.validation.constraints.Email;

@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = "cpf")})
public class Client {

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

    @NotBlank(message = "Name is required")
    private String name;

    @Email(message = "Invalid e-mail")
    @Column(unique = true, nullable = false)
    private String email;

    @CPF(message = "Invalid CPF")
    @Column(unique = true, nullable = false)
    private String cpf;

    @NotBlank(message = "Birthday is required")
    private String birthday;

    public Client(String name, String email, String cpf, String birthday) {
        this.name = name;
        this.email = email;
        this.cpf = cpf;
        this.birthday = birthday;
    }

    public Client() {
    }

    public int getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getCpf() {
        return cpf;
    }

    public void setCpf(String cpf) {
        this.cpf = cpf;
    }

    public String getBirthday() {
        return birthday;
    }

    public void setBirthday(String birthday) {
        this.birthday = birthday;
    }

}


A classe ClientResponse foi criada para lidar com as respostas da API. Assim seria possível enviar a resposta como um objeto JSON e mandar uma mensagem personalizada a cada requisição.


package br.com.edsonajeje.cadatroAPI.model.entities;

public class ClientResponse {

    private Client client;
    private String message;

    public ClientResponse(Client client, String message) {
        this.client = client;
        this.message = message;
    }

    public ClientResponse() {
    }

    public Client getClient() {
        return client;
    }

    public void setClient(Client client) {
        this.client = client;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}


ClientRepository


Para a interface ClientRepository, foram implementados dois métodos que corresponderiam a querys especificas da nossa API, que seriam a busca de um cliente pelo e-mail e pelo CPF.

Assim ela foi criada como uma interface normal que estende a interface CrudRepository do Spring. Essa interface recebe um como tipos genéricos uma classe que se refere à tabela do banco e um objeto que seja do mesmo tipo da chave primária deste.

A criação dos métodos customizados utilizam o SQL com mudanças pontuais. Usa-se o annotation @Query que recebe como parâmetro uma query. Por exemplo, para realizar a busca do cliente pelo CPF, uma query normal em SQL seria “SELECT * FROM client c WHERE c.cpf LIKE ...” onde os três pontinhos são substituídos pelo valor buscado. Para essa annotation, a mesma query ficaria assim: “SELECT c FROM Client c WHERE c.cpf LIKE ?1”. Nessa query o primeiro c identifica o objeto inteiro que será buscado, equivalendo à tupla do banco, o Client é a classe e o ?1 indica que o primeiro argumento da função será passado aqui como parâmetro da busca.


package br.com.edsonajeje.cadatroAPI.model.repositories;

import br.com.edsonajeje.cadatroAPI.model.entities.Client;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import java.util.Optional;


public interface ClientRepository extends CrudRepository<Client, Integer> {

    @Query("select c from Client c where c.cpf like ?1")
    Optional<Client> findClientByCpf(String cpf);

    @Query("select c from Client c where c.email like ?1")
    Optional<Client> findClientByEmail(String email);
}


Um ponto importante que deve ser falado sobre esse código é o retorno como Optional<Client>. Com essa classe estou dizendo ao java que essa busca no banco pode me retornar um objeto Client ou não me retornar nada. Assim eu posso tratar para as duas possibilidades de retorno do banco.


ClientController


Uma vez que estamos com tudo pronto, podemos começar a fazer os nossos endpoints. No padrão MVC esse papel é desempenhado pelo Controller. Então vamos criar um pacote controllers dentro do pacote principal, no mesmo nível do pacote model. Dentro dele vamos criar a classe ClientController.

Essa classe irá receber as chamadas e realizar os retornos. Para que ela faça esse papel ela recebe o annotation @RestController. Ele informa ao spring que esta classe é um endpoint. Para definir a rota para qual esse endpoint responde, marcamos a classe com @RequestMapping e como parâmetro desse annotation informamos uma string com o endpoint. Para a nossa API, na rota de clientes vamos utilizar o “/api/v1/clients”. Costumo versionar as APIs. Então com o api, informo que se trata de uma api, com o v1 informo a versão do código e com o clients o objeto que será mantido pela rota.


@RestController
@RequestMapping("/api/v1/clients")
public class ClientController {


Como o padrão MVC e as boas práticas do Clean Code pedem, vamos utilizar a inversão de dependência. Então não faremos uma instanciação de um objeto do tipo ClientRepository, mas deixaremos que o spring gerencie isso através da annotation @Autowired.


@Autowired
private ClientRepository clientRepository;


Alguns pontos são importantes sobre esse código. Primeiro, todos os métodos retornam um objeto do tipo ResponseEntity. Esse objeto recebe um tipo genérico e cuida da configuração da resposta Http que será enviada. Então se espera-se que o método retorne com um objeto do tipo Client, esse método retornará um ResponseEntity<Client>. Assim é possível colocar o Client como parte da resposta, configurar o código Http e deixar que o spring cuide de todo o resto.

Para tornar nossa resposta mais interessante, anteriormente criamos a classe ClientResponse. Ela será utilizada aqui para isso, pois recebe como atributos um objeto do tipo Client e uma mensagem do tipo string.


Criação das rotas


Para configurar então uma rota vamos criar um método com esse tipo de resposta que falamos e marcar esse método com os annotations responsáveis por seu gerenciamento pelo spring. Para a rota GET que retornará todos os clientes vamos criar o método getClients. Ele usará o clientRepository que foi injetado pelo spring e dele o método findAll. Retornando assim um objeto do tipo Iterable<Client>. Para marcá-lo como uma rota do tipo GET usamos a annotation @GetMapping.


@GetMapping
public ResponseEntity<Iterable<Client>> getClients() {
    var data = clientRepository.findAll();
    return new ResponseEntity<>(data, HttpStatus.OK);
}


A rota para criação de um novo cliente no banco é um pouquinho mais complexa, porque ela precisa tratar as possibilidades de e-mail e CPF duplicados. Para isso já implementamos previamente na interface clientRepository dois métodos especialmente pensados para esse fim.


O fluxo desse método será então o seguinte: a aplicação cliente fará uma requisição do tipo POST para a rota “api/v1/clients”. Ela enviará via corpo da requisição um objeto JSON contendo o nome do cliente, o e-mail, o CPF e a data de nascimento. Quando chegar na rota, o método createClient vai buscar no banco se existe algum cliente com esse CPF e se existe algum cliente com esse e-mail informados no objeto JSON. Serão feitas duas consultas separadas porque podem ser dois clientes diferentes que tenham esses dados no banco. Essas consultas vão retornar um objeto do tipo Optional<Client>. E essa classe possui um método isPresent que retorna um booleano informando true se contiver um objeto Client e false se não contiver.


Aí você que está lendo pode pensar: “Mas eu já não marquei com o annotation @Column(unique = true)? Isso já não fez o banco tomar as precauções para que não fosse possível inserir valores duplicados? Por que está fazendo essa verificação toda?”.


Eu te respondo: Essa abordagem realmente garante que o banco não insira valores duplicados. Mas se tentarmos inserir um valor duplicado, o banco vai estourar um erro e nossa API vai retornar para a aplicação cliente um status 500.

O http status code 500 indica que houve um erro no servidor. Mas não foi isso que ocorreu, ocorreu um status 401, Bad Request, ou seja, o cliente tentou realizar uma operação que não possuía todas as informações necessárias. Isso não é um erro do servidor, mas sim do cliente.

Para resolver isso tomei esse caminho de fazer a verificação antes de tentar inserir o cliente no banco. Isso me garante que a API retorne o status code correto e não estoure um erro para o cliente.

Seria possível utilizar um bloco try/catch e capturar o erro vindo do banco. Porém esse código seria muito mais complexo e não é o objetivo agora.

Segue a seguir como ficou esse código. A diferença do annotation do código anterior é que este utiliza o @PostMapping e ele recebe como parâmetro o argumento produces com o valor “application/json”, que, apesar de redundante, é para garantir que a resposta seja em formato JSON.


@PostMapping(produces="application/json")
public ResponseEntity<ClientResponse> createClient(@Valid @RequestBody Client client) {
    Optional<Client> cpf = clientRepository.findClientByCpf(client.getCpf());
    Optional<Client> email = clientRepository.findClientByEmail(client.getEmail());
    ClientResponse response = new ClientResponse();
    if(cpf.isPresent() || email.isPresent()){
        if (cpf.isPresent() && email.isPresent()) {
            response.setMessage("E-mail is required and must be unique. " +
                    "CPF is required and must be unique");
        } else if(email.isEmpty()) {
            response.setMessage("CPF is required and must be unique");
        } else {
            response.setMessage("E-mail is required and must be unique.");
        }
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }
    else{
        var newClient = clientRepository.save(client);
        response.setClient(newClient);
        response.setMessage("Success");
        return new ResponseEntity<>(response, HttpStatus.CREATED);
    }
}


O próximo método trata da rota para o verbo PUT. Ela será a mesma rota para retornar um cliente pelo id, porém como o verbo Http é diferente, o Spring vai direcionar para o método correto. Este método é marcado com o @PutMapping que recebe o argumento path com o valor “/{id}”. Isso informa para o método que um dos parâmetros será passado via path da rota e que seu nome é id e para que o spring entenda que parâmetro é esse, marcamos ele com o @PathVariable.

Como o verbo PUT faz uma update em uma tupla da tabela, os dados desse cliente que serão atualizados virão via corpo da requisição. E o método updateClient recebe como parâmetro um objeto Client marcado como @RequestBody e o Spring é inteligente o suficiente para entender do que se trata.

O que esse método updateClient faz é buscar um cliente pelo id informado e caso esse cliente exista no banco, fazemos o update desse dado.

Para garantir que o update será feito na tupla correta do banco, forçamos o id do objeto para o id informado na rota. Assim, caso o objeto não tenha vindo com um id no corpo, garantimos que o método save do clientRepository vai fazer um update e não tentar criar um novo cliente.


@PutMapping(path = "/{id}")
public ResponseEntity<ClientResponse> updateClient(@RequestBody Client client, @PathVariable int id) {
    boolean data = clientRepository.existsById(id);
    ClientResponse response = new ClientResponse();
    if(data){
        client.setId(id);
        clientRepository.save(client);
        response.setClient(client);
        response.setMessage("Success");
        return new ResponseEntity<>(response, HttpStatus.OK);
    }else{
        response.setMessage("Client id: "+id+" cannot be found");
        return new ResponseEntity<>(response, HttpStatus.NO_CONTENT);
    }
}


Caso esse id não exista no banco, ao invés de criar um novo cliente, o que seria um comportamento errado para o verbo PUT, retornamos o status No Content, mostrando que não foi localizado o cliente com o id informado.


O próximo método utiliza a mesma rota do PUT, porém com verbo GET. Essa rota vai retornar um cliente informando um id como parâmetro da rota. O método é muito parecido com o updateClient, então fica fácil entender como e o que ele faz. Nele buscamos um cliente pelo id utilizando o findById do clientRepository e caso ele retorne com um cliente enviamos esse retorno para a aplicação cliente com um status 200.


@GetMapping(path = "/{id}")
public ResponseEntity<ClientResponse> getClientById(@PathVariable int id) {
    var client = clientRepository.findById(id);
    ClientResponse response = new ClientResponse();
    if(client.isPresent()){
        response.setClient(client.get());
        response.setMessage("Success");
        return new ResponseEntity<>(response, HttpStatus.OK);
    }
    response.setMessage("Client id: "+id+" cannot be found");
    return new ResponseEntity<>(response, HttpStatus.NO_CONTENT);
}


O último método da rota clients é o método deleteClient. Ele também utiliza a rota “/{id}” mas o método Http é o DELETE. Marcamos o deleteClient com o @DeleteMapping e utilizamos o método delete do clientRepository.


@DeleteMapping(path = "/{id}")
public ResponseEntity<ClientResponse> deleteClient(@PathVariable int id) {
    Optional<Client> data = clientRepository.findById(id);
    ClientResponse response = new ClientResponse();
    if(data.isPresent()){
        clientRepository.delete(data.get());
        response.setMessage("Client deleted");
        return new ResponseEntity<>(response, HttpStatus.OK);
    }
    response.setMessage("Client id: "+id+" cannot be found");
    return new ResponseEntity<>(response, HttpStatus.NO_CONTENT);
}


É importante salientar alguns pontos. Em uma API real existem métodos de segurança para trabalhar com rotas autenticadas, documentação e vários outros middlewares que trabalham para a segurança dos dados e do banco e para garantir que a informação chegue somente a quem realmente tem acesso a ela.

O que foi implementado aqui é um CRUD simples para uma rota. Para ser uma API é preciso muito mais coisas. E esse código pode não ter ficado o mais correto do mundo, mas eu não sabia nada de spring, JPA e Hibernate. Aprendi pra fazer esse código. Então mesmo que esteja cheio de erros, estou feliz com o resultado 😊


0
145

Comentários (1)

0
M

Marcos Coelho

06/04/2021 13:32

Muito bem explicado e detalhado, ajudou bastante !

None

Brasil