API RESTful com Spring Boot

Código fonte disponível em: https://github.com/waltercoan/ProjetoSpring

Acesse a aplicação em execução: https://springboot-waltercoan.herokuapp.com

O objetivo deste material é demonstrar como criar uma aplicação web que disponibilize o acesso através de API RESTful. API (Application program interfaces) são funções dentro de um sistema que tem um meio de troca de informação padronizado, portanto, servem como ponto de integração entre sistemas. Um exemplo simples é a nota fiscal eletrônica do Brasil, todas as empresas que comercializam produtos ou prestam algum tipo de serviço precisam informar ao governo o que foi vendido para controle do pagamento de impostos. Isso é feito através do processo de emissão da NF-e, que consiste na integração da nota fiscal emitida pelos sistemas de gestão dos mais diversos tipos, com o sistema do SEFAZ de cada estado. Essa integração acontece pela chamada da API, hoje normalmente utilizando uma requisição do protocolo HTTP/HTTPS para um endereço. Essa chamada precisa seguir uma padronização, conforme a definição da API, basicamente os dois modelos mais comuns são SOAP e o RESTful. SOAP é um padrão de troca de mensagens baseado no padrão XML (Extensible Markup Language), onde neste padrão são realizadas trocas de envelopes de dados (mais detalhes https://en.wikipedia.org/wiki/SOAP), sendo esse o padrão adotado para a NF-e no Brasil, principalmente porque SOAP permite uma forte validação do formato em que os envelopes de dados foram construídos, requisito importante para validar documentos fiscais como a nota fiscal. Mas o SOAP tem também suas desvantagens, pois como existe todo um padrão para construção dos envelopes de dados, isso trás uma sobrecarga no volume de dados transportados, além de uma maior complexidade. O padrão RESTful é o mais adotado na construção de integração entre sistemas, principalmente por usa simplicidade. REST significa Representational State Transfer), o que significa que o estado da aplicação será transferido de uma ponta até outra no processo de integração. Diferente do SOAP o REST utiliza como padrão de formatação dos dados o JSON (JavaScript Object Notation – https://en.wikipedia.org/wiki/JSON) que consiste num padrão mais simples para a padronização dos dados. Outras características do padrão JSON são:

  • Utilização dos verbos do protocolo HTTP: GET – POST – PUT – DELETE
  • Padroniza o nome de fontes de dados com substantivos
  • Não utiliza verbos nos endereços para definição de métodos, pois se utiliza dos verbos do protocolo HTTP
  • Preconiza que a comunicação cliente-servidor deve ser Stateless, portanto o estado não deve ser mantido entre requisições
  • Não deve o mínimo acoplamento e interdependência entre as requisições para o servidor
  • Utilização dos códigos de retorno do HTTP e dos media-types

Existem novos padrões que estão sendo amplamente utilizados na substituição do RESTFul, como o GRPC bastante utilizando, por exemplo pelo NETFLIX, em arquiteturas de micro-serviços para comunicação entre componentes (https://grpc.io).

Baseado no primeiro projeto de Spring Boot, vamos desenvolver o código de uma RESTful API para permitir que outros sistemas se integrem ao desenvolvido. Portanto precisamos verificar os seguintes pontos:

Dentro do pacote model, precisamos ter nossas classes entity criadas e com o mapeamento objeto relacional corretamente feito.

Para cada entidade que desejamos permitir acesso pela API RESTful, precisamos construir uma interface repository, para dar acesso ao dados no banco de dados, conforme figura abaixo.

Agora que já verificamos os componentes base para construção da nossa API RESTful, vamos iniciar o desenvolvimento dela em si. Nosso objetivo é permitir o acesso aos dados da entidade paciente, para isso, vamos criar um novo pacote chamado api, onde ficarão as classes com o código fonte. Para isso clique com o botão direito do mouse sobre o pacote projetospring -> opção new -> Package.

No campo do nome do pacote informe o final .api e confirme no botão finish.

Clique com o botão direito sobre o novo pacote api, e selecione a opção New -> Class.

No campo name informe PacienteControllerAPI e confirme no botão finish.

Método GET

O código a seguir a presenta nosso primeiro método da API RESTFul, utilizando o verbo GET do protocolo HTTP, vamos expor uma lista de objetos JSON que representará todos os registros com os dados dos pacientes armazenados no banco de dados. Observe as seguintes linhas de código:

  • Na linha 15 inserimos a anotação @RestController, similar a anotação @Controller que utilizamos no código anterior, mas com o objetivo de informar ao Spring Boot que esta classe deverá se comportar como a API RESTful;
  • Na linha 16 utilizamos a anotação @RequestMapping para informar que o endereço base para essa API será http://localhost:8080/api/pacientes, seguindo o padrão das API RESTful, onde o nome do recurso deve ser identificado por um substantivo;
  • Nas linhas 18 e 19, realizamos o autowired com o repositório do Paciente, para ter acesso aos dados do banco de dados;
  • Na linha 21 utilizamos a anotação @GetMapping para informar que esse método deverá responder a requisições utilizando o verbo GET do protocolo HTTP
  • Na linha 22 observe que o método listarPacientes, retorna o objeto ResponseEntity cujo dentro dele haverá uma lista de instâncias do objeto Paciente. Esse objeto automatiza o processo de conversão dos objetos Java no padrão JSON
  • Nas linhas 23 e 24 – utilizamos o método findAll() do repositório do paciente, para ter acesso a uma lista de instâncias do objeto Paciente, que na linha seguinte é encapsulada dentro de uma nova instância do objeto ResponseEntity e retornado como resposta ao cliente, com o HTML return code OK (200)
package br.com.faltoupontoevirgula.projetospring.api;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import br.com.faltoupontoevirgula.projetospring.model.Paciente;
import br.com.faltoupontoevirgula.projetospring.repository.PacienteRepository;

@RestController
@RequestMapping("/api/pacientes")
public class PacienteControllerAPI {
    @Autowired
    private PacienteRepository pacienteRepository;
   
    @GetMapping
    public ResponseEntity<List<Paciente>> listarPacientes() {
        List<Paciente> lista = pacienteRepository.findAll();
        return new ResponseEntity<List<Paciente>>(lista,HttpStatus.OK);
    }
}

Caso você salve e republique este código, acesse a interface de cadastro de pacientes e cadastre alguns registros e em seguida acesse a URL: http://localhost:8080/api/pacientes o resultado deverá ser similar à imagem abaixo:

Observe que o objeto JSON tem um padrão:

  • Os colchetes [ ] representam uma lista de valores ou objetos
  • As chaves { } representam a estrutura de uma instância de um objeto
  • Os atributos são descritos no padrão CHAVE : VALOR onde sempre a chave deve ter seu valor entre aspas duplas e o valor depende do tipo, sendo separados por vírgulas ex: “id”:1,”nome”:”Mariazinha”

Método POST

Agora que o verbo GET da nossa API RESTful esta pronto, podemos partir para o verbo POST seu objetivo é inserir novos registros no sistema, para isso conforme o código a seguir inserimos as seguintes linhas em nosso controlador.

  • Na linha 29 – utilizamos a anotação @PostMapping para que este método responsa ao verbo POST do HTTP
  • Na linha 30 – definimos a assinatura do método, primeiro o retorno será uma instância da classe ResponseEntity assim como o método listar, mas com a diferença que utilizamos o operador diamante com uma interrogação , desta forma o tipo do dado genérico que a classe deveria responder fica com o valor NULL. Observe que o método save recebe por parâmetro de entrada a classe Paciente e recebe a anotação @RequestBody, portanto o objeto JSON que representará os dados do novo paciente, deverá ser enviado no corpo Body, da requisição HTTP.
  • Nas linhas 31 e 32 utilizamos o repositório da classe paciente para persistir o objeto e construímos um retorno com os métodos estáticos da classe ResponseEntity, para retornar o HTTP responde code 200, indicando sucesso.
package br.com.faltoupontoevirgula.projetospring.api;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import br.com.faltoupontoevirgula.projetospring.model.Paciente;
import br.com.faltoupontoevirgula.projetospring.repository.PacienteRepository;

@RestController
@RequestMapping("/api/pacientes")
public class PacienteControllerAPI {
    @Autowired
    private PacienteRepository pacienteRepository;
   
    @GetMapping
    public ResponseEntity<List<Paciente>> listarPacientes() {
        List<Paciente> lista = pacienteRepository.findAll();
        return new ResponseEntity<List<Paciente>>(lista,HttpStatus.OK);
    }
   
    @PostMapping
    public ResponseEntity<?> save(@RequestBody Paciente paciente){
        pacienteRepository.save(paciente);
        return ResponseEntity.ok().build();
    }
}

Para realizar o teste deste novo método, necessitamos de alguma ferramenta que simplifique a construção da requisição POST do HTTP, para isso podemos utilizar extensão para os navegadores Chrome ou Firefox chamada Postman (https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop) conforme a figura abaixo:

  • Definimos que a requisição utilizará o verbo POST, e em seguida informamos a URL http://localhost:8080/api/pacientes
  • Na aba Body, selecionamos a opção raw e o tipo do dado como JSON (application/json)
  • No campo abaixo, informamos o valor da requisição com o objeto JSON conforme exemplo: {“nome”:”Pedrinho”,”sexo”:”Masculino”}
  • Por fim, acione o botão SEND, e observe se o retorno foi o código 200 (OK)

Para ter certeza que o método funcionou corretamente, podemos utilizar o POSTMAN para executar uma chamada com o verbo GET para a mesma URL e o resultado deverá ser similar a imagem a seguir.

Método PUT

Agora vamos trabalhar no verbo PUT, que será responsável por realizar o UPDATE dos registros no banco de dados. O código a seguir, apresenta o método update, com o seguinte código:

  • Na linha 38 – inserimos a anotação @PutMapping(path=”/{id}”) que conecta o método update com o verbo PUT do HTTP, e alteramos a URL base para incluir uma nova barra e uma variável chamada ID. Portanto a chamada HTTP fica: http://localhost:8080/api/pacientes/1 onde 1 representa o ID do objeto que se deseja alterar
  • Na linha 39 – definimos a assinatura do método update, onde o retorno será uma instância do objeto ResponseEntity, e os parâmetros são @PathVariable(“id”)long id, @RequestBody Paciente newPaciente: a anotação @PathVariable liga a variável id criada na URL pela anotação @PutMapping à variável long id do método, e o parâmetro @RequestBody Paciente newPaciente receberá pelo corpo body da requisição os dados do objeto que se deseja atualizar.
  • Nas linhas 40 à 42 utilizamos o repositório para buscar a instância da classe Paciente pelo id recebido pelo método update por parâmetro, como essa busca pode falhar o repositório do Spring utiliza o objeto Optional que pode ou não conter o retorno de acordo com o resultado da busca no banco de dados. Em seguida utilizamos a estrutura de decisão IF para verificar se o método isPresent() retorna o valor falso, o que indica que a busca falhou no banco de dados, portanto retornamos o status code do HTTP não encontrado 404.
  • Caso o código chegue à linha 44 – recuperamos do objeto Optional a instância real da classe Paciente e em seguida atualizamos cada atributo com o valor da nova instância de paciente que foi recebido pela passagem de parâmetro da requisição.
  • Por fim na linha 49 executamos o método save do repositório para salvar as alterações no objeto e retornarmos o status code 200 como indicação de sucesso para a alteração
package br.com.faltoupontoevirgula.projetospring.api;

import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import br.com.faltoupontoevirgula.projetospring.model.Paciente;
import br.com.faltoupontoevirgula.projetospring.repository.PacienteRepository;

@RestController
@RequestMapping("/api/pacientes")
public class PacienteControllerAPI {
    @Autowired
    private PacienteRepository pacienteRepository;
   
    @GetMapping
    public ResponseEntity<List<Paciente>> listarPacientes() {
        List<Paciente> lista = pacienteRepository.findAll();
        return new ResponseEntity<List<Paciente>>(lista,HttpStatus.OK);
    }
   
    @PostMapping
    public ResponseEntity<?> save(@RequestBody Paciente paciente){
        pacienteRepository.save(paciente);
        return ResponseEntity.ok().build();
    }
   
    @PutMapping(path="/{id}")
    public ResponseEntity<?> update(@PathVariable("id")long id, @RequestBody Paciente newPaciente){
        Optional<Paciente> talvezPaciente = pacienteRepository.findById(id);
        if (!talvezPaciente.isPresent())
            return ResponseEntity.notFound().build();
       
        Paciente oldPaciente = talvezPaciente.get();
       
        oldPaciente.setNome(newPaciente.getNome());
        oldPaciente.setSexo(newPaciente.getSexo());
       
        pacienteRepository.save(oldPaciente);
       
        return ResponseEntity.ok().build();
    }
   
   
}

Método DELETE

Agora podemos testar essa nova funcionalidade, para isso vamos primeiro, como ilustrado na figura a seguir, realizar uma chamada POST para inserir um novo registro no sistema.

Em seguida, executamos uma chamada GET, para confirmar que o registro foi persistido pelo sistema.

Agora, vamos testar nosso novo método PUT, observe que no campo da URL inserimos a chave id do objeto que desejamos modificar e no corpo o objeto com os campos que serão alterados, conforme imagem a seguir.

Para confirmar se as alterações foram realizadas, faça uma chamada GET para o serviço e verifique se o registro foi modificado.

Para concluir a implementação básica da API RESTful, vamos agora construir o método que irá responder ao verbo DELETE do HTTP, para isso foram feitas as seguintes modificações conforme exemplo abaixo:

  • Linha 55 – o método delete recebe a anotação @DeleteMapping(path=”/{id}”) para ser invocado toda vez que o verbo DELETE for enviado em uma requisição HTTP, importante que pela URL deverá ser passado o id o objeto a ser excluído.
  • Linha 56 – a assinatura do método delete retorna uma instância da classe ResponseEntity e tem como parâmetro de entrada apenas o @PathVariable(“id”) long id que será atribuído com o valor passado na URL
  • Nas linhas 57 à 63, o método utiliza o repositório para buscar a instância do objeto paciente pelo id, confirma se a busca foi realizada com sucesso e em seguida realiza a exclusão, retornando por fim o status code 200.
package br.com.faltoupontoevirgula.projetospring.api;

import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import br.com.faltoupontoevirgula.projetospring.model.Paciente;
import br.com.faltoupontoevirgula.projetospring.repository.PacienteRepository;

@RestController
@RequestMapping("/api/pacientes")
public class PacienteControllerAPI {
    @Autowired
    private PacienteRepository pacienteRepository;
   
    @GetMapping
    public ResponseEntity<List<Paciente>> listarPacientes() {
        List<Paciente> lista = pacienteRepository.findAll();
        return new ResponseEntity<List<Paciente>>(lista,HttpStatus.OK);
    }
   
    @PostMapping
    public ResponseEntity<?> save(@RequestBody Paciente paciente){
        pacienteRepository.save(paciente);
        return ResponseEntity.ok().build();
    }
   
    @PutMapping(path="/{id}")
    public ResponseEntity<?> update(@PathVariable("id")long id, @RequestBody Paciente newPaciente){
        Optional<Paciente> talvezPaciente = pacienteRepository.findById(id);
        if (!talvezPaciente.isPresent())
            return ResponseEntity.notFound().build();
       
        Paciente oldPaciente = talvezPaciente.get();
       
        oldPaciente.setNome(newPaciente.getNome());
        oldPaciente.setSexo(newPaciente.getSexo());
       
        pacienteRepository.save(oldPaciente);
       
        return ResponseEntity.ok().build();
    }
   
    @DeleteMapping(path="/{id}")
    public ResponseEntity<?> delete(@PathVariable("id") long id){
        Optional<Paciente> talvezPaciente = pacienteRepository.findById(id);
        if (!talvezPaciente.isPresent())
            return ResponseEntity.notFound().build();
       
        pacienteRepository.delete(talvezPaciente.get());
       
        return ResponseEntity.ok().build();
    }
}

Para validar o funcionamento do método delete, faça inicialmente uma chamada POST para inserir um novo registro no sistema, conforme imagem abaixo.

Em seguida realize a chamada para o método DELETE, passando o id do registro na URL no seguinte formato: http://localhost:8080/api/pacientes/1 Confirme se o status de retorno foi 200.

Por fim, execute novamente uma requisição com o verbo GET para confirmar se o registro foi excluído do sistema.

Criando testes unitários para a API RESTful

Para garantir o correto funcionamento da nossa API, precisamos implementar uma classe de teste unitário que verifique o funcionamento dos métodos. Para isso criamos uma nova classe de testes dentro do pacote src/test/java (br.com.faltoupontoevirgula.projetospring) chamada APIApplicationTests cujo código esta a seguir.

package br.com.faltoupontoevirgula.projetospring;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

import br.com.faltoupontoevirgula.projetospring.api.PacienteControllerAPI;



@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class APIApplicationTests {

    @Autowired
    private PacienteControllerAPI pacienteControllerAPI;
    @Autowired
    private MockMvc mockMvc;

    @Test
    public void contextLoads() {
        assertThat(pacienteControllerAPI).isNotNull();
    }

    @Test
    public void pacienteControllerAPIPOSTGETTest() throws Exception {
        mockMvc.perform(post("/api/pacientes")
                .content("{"nome":"zezinho", "sexo":"Masculino"}")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk());

        mockMvc.perform(get("/api/pacientes"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].nome", is("zezinho")))
                .andExpect(jsonPath("$[0].sexo", is("Masculino")));
    }

    @Test
    public void pacienteControllerAPIPOSTPUTTest() throws Exception {
        mockMvc.perform(post("/api/pacientes")
                .content("{"nome":"Maria", "sexo":"Feminino"}")
                .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk());

        mockMvc.perform(put("/api/pacientes/1")
                .content("{"nome":"Maria da Silva", "sexo":"Feminino"}")
                .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk());

        mockMvc.perform(get("/api/pacientes"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].nome", is("Maria da Silva")))
                .andExpect(jsonPath("$[0].sexo", is("Feminino")));
    }

    @After
    public void cleanDb() throws Exception {
        MvcResult result =
                mockMvc.perform(get("/api/pacientes"))
                .andExpect(status().isOk())
                .andReturn();

        String resultStr = result.getResponse().getContentAsString();

        JSONArray objArrayJson = new JSONArray(resultStr);

        int size = objArrayJson.length();

        for(int i=0;i<size;i++) {
            JSONObject objJson = objArrayJson.getJSONObject(i);

            mockMvc.perform(delete("/api/pacientes/" + objJson.getString("id")))
            .andExpect(status().isOk());
        }
    }
}
  • Nas linhas 40 à 42, criamos um teste básico inicial que verifica apenas se é possível instanciar o controlador da API do Paciente
  • Nas linhas 46 à 49, criamos um teste para criar um novo registro através do verbo POST, observe que o JSON é passado no corpo da requisição, e basicamente esperamos que o status code retornado seja 200.
  • Nas linhas 51 à 54, criamos um teste que utiliza o verbo GET para checar se o registro foi inserido corretamente no sistema. Ao fazer a requisição, verificamos se o status de retorno é OK 200, e em seguida verificamos se o conteúdo do JSON retornado contém um objeto na primeira posição com os valores de nome zezinho e sexo Masculino
  • Nas linhas 59 à 62, criamos um teste que realiza novamente a requisição ao verbo POST para inserir um novo objeto paciente no sistema
  • Nas linhas 64 à 67, criamos um teste que realiza a requisição do verbo PUT para modificar o nome do paciente
  • Nas linhas 69 à 72, criamos um último teste que realiza a requisição com o verbo GET para conferir se o nome do paciente foi modificado corretamente

Importante: um dos conceitos básico dos testes unitários consiste que não deve haver dependência de estados entre os métodos de teste, portanto criamos um método cleanDb() que foi anotado com o @After, portanto esse método é automaticamente chamado depois da execução de cada um dos teste. Como seu nome já diz ele fica responsável por limpar o banco de dados depois de cada teste, para isso ele realiza uma requisição com o verbo GET, e armazena o JSON resultante em uma variável chamada result. Esse conteúdo é transformado para String, e em seguida utilizando os objetos JSONArray e JSONObject, o conteúdo do objeto JSON é lido e para cada instância retornada é realizada uma requisição para o servidor com o verbo DELETE, excluindo o registro inserido um a um.