Construindo uma API RESTful com Java e Spring Framework — Parte 1
Hoje em dia, está cada vez mais comum termos aplicações que funcionam online, em navegadores ou dispositivos móveis. Essas aplicações tem por objetivo consumir informação por meio de interfaces que implementam uma série rotinas e padrões que chamamos de API.
O acrônimo API vem da expressão em inglês Application Programming Interface (em português, Interface de Programação de Aplicações). Uma API é um conjunto de padrões e regras documentadas para que uma aplicação X possa utilizar funcionalidades de uma aplicação Y sem precisar conhecer os detalhes da implementação dessa aplicação X.
Ainda está obscuro? Para facilitar o entendimento, vamos imaginar o dia-a-dia de uma empresa que possui um e-commerce. Os desenvolvedores que trabalham na solução tem como objetivo criar a dinâmica da loja, como criar, atualizar, deletar os produtos internamente e mostrar os produtos para os clientes. Essas funcionalidades podem ser criadas em uma aplicação do lado do servidor como se fosse uma API, de forma que o site do e-commerce possa usar essas informações.
Agora que já sabemos o que é uma API e para que utilizamos uma, precisamos entender os protocolos que são utilizadas para a comunicação entre as aplicações e como os dados trafegados podem ser representados.
HTTP, REST e representações de dados em APIs
HTTP e REST são a mesma coisa?
O principal protocolo de comunicação na Web é o HTTP. Ele funciona como um protocolo de requisição-resposta em um modelo que chamamos de cliente-servidor. No exemplo acima, do e-commerce, o navegador que é usado para acessar o site seria o cliente e o computador ou máquina virtual em algum serviço de cloud em que a API está hospedada é o servidor. Assim, o cliente manda uma requisição HTTP para o servidor e o servidor, com recursos e conteúdos próprios, retorna uma mensagem de resposta para o cliente.
O protocolo HTTP tem sido usado desde 1990 e a versão atual do protocolo é o HTTP/3. O protocolo define oito métodos que determinam ações a serem efetuadas no momento da requisição de algum recurso ao servidor. Desses oito, os 4 mais utilizados são:
- GET: método utilizado para ler e recuperar dados. Requisita uma representação do recurso especificado e retorna essa representação.
- POST: método utilizado para criar um novo recurso. Envia dados ao servidor. O tipo do corpo da solicitação é indicado pelo cabeçalho
Content-Type
. - PUT: cria um novo recurso ou substitui uma representação do recurso de destino com os novos dados. A diferença entre
PUT
ePOST
é quePUT
é idempotente: ao chamá-lo uma ou várias vezes sucessivamente o efeito é o mesmo, enquanto se chamar oPOST
repetidamente pode ter efeitos adicionais. Por exemplo, se criarmos um produto comPOST
, se a URL definida na API for chamada 20 vezes, 20 produtos serão criados e cada um deles terá um ID diferente. Já o com oPUT
se você executar a URL definida na API 20 vezes, o resultado tem que ser o mesmo: o mesmo produto atualizado 20 vezes. - DELETE: exclui o recurso.
Baseado nesses métodos , o servidor deve processar cada uma das requisições e retornar uma resposta adequada. O conteúdo da resposta pode estar no formato XML, JSON, YAML, texto, dentre outros. E essas as respostas são separadas em cinco grupos:
- 1XX — Informações Gerais
- 2XX — Sucesso
- 3XX — Redirecionamento
- 4XX — Erro no cliente
- 5XX — Erro no servidor
Mas isso não é REST? Não, não é. REST, acrônimo de Representational State Transfer, é uma abstração dessa arquitetura que detalhamos acima. É um estilo de arquitetura de software que define uma série de restrições para a criação de web services (serviços Web), ou seja, restringe como seus componentes devem interagir entre si. Esse termo foi introduzido e definido por Roy Fielding em sua tese de doutorado no final dos anos 90 e início dos anos 2000.
Na tese, Fielding definiu os princípios REST que eram conhecidos como “modelo de objeto HTTP” e passaram a ser utilizados no projeto dos padrões HTTP 1.1 e URI (Uniform Resource Identifiers). Dessa forma, podemos dizer que em sua semântica, o REST utiliza-se métodos HTTP. Além desse conceito, vale lembrar também que um serviço REST deve ser Stateless: toda requisição deve ser autossuficiente, ou seja, cada requisição é um requisição diferente e independente. Não deve existir na requisição nenhuma forma de guardar o estado de uma informação.
E o que é ser RESTful? Dizemos que uma API é RESTful, se garantimos que implementação da API está de acordo com essa arquitetura REST explicada acima. Conceitualmente, nos serviços RESTful, tanto os dados quanto as funcionalidades são considerados recursos e ficam acessíveis aos clientes através da utilização de URIs. Essas URI’s normalmente são endereços na web que identificam tanto o servidor no qual a aplicação está hospedada quanto a própria aplicação e qual dos recursos oferecidos pela mesma está sendo solicitado.
Dessa forma, expor as funcionalidades da sua API (ou funcionalidades dos seus serviços, se você preferir) no modo RESTful, os princípios REST e suas restrições se aplicam a você também. Com todos os conceitos na mesa, vamos colocar a mão na massa e construir uma API? Iremos utilizar a linguagem Java (versão 11) com Spring Framework como base da API. Além disso, usaremos como ferramentas:
- Apache Maven (para gestão de dependências)
- Postman (para execução de testes e requisições em geral na API)
- JUnit5 (para testes unitários e de integração)
- Lombok (para reduzir código boilerplate)
- Log4j (para adicionar logs na aplicação)
- TravisCI (para integração contínua)
Todo o código produzido no artigo, em uma versão simplificada da API, pode ser acessada no repositório abaixo:
Caso você queira pular o tutorial e ver o código final, que é uma versão otimizada aplicando boas práticas, você pode acessar o repositório no Github:
Construindo nossa API
Primeiros passos com a API RESTful
A API deve criar, atualizar, deletar e listar viagens. Além disso, deve calcular estatísticas sobre as os tickets de viagem criados. Nesse exemplo, não vamos tratar camada de persistência, e, por isso, de forma ilustrativa, a entidade Travel conterá um campo chamado id. A API terá os seguintes endpoints:
POST/api-travels/travels: cria uma viagem.
Body da requisição:
{
"id": 1,
"orderNumber": "220788",
"amount": "22.88",
"startDate": "2019–09–11T09:59:51.312Z",
"type": "ONE-WAY"
}
Em que:
- id: número único da transação;
- orderNumber: número de identificação de uma viagem no sistema interno.
- amount: valor da viagem; deve ser uma String de tamanho arbitrário que pode ser parseada como um BigDecimal;
- startDate: data de início da viagem no formato ISO 8601 YYYY-MM-DDThh:mm:ss.sssZ no timezone local.
- endDate: data de fim da viagem no formato ISO 8601 YYYY-MM-DDThh:mm:ss.sssZ no timezone local. Pode ser nulo se a viagem é só de ida.
- type: se a viagem é apenas de ida (ONE-WAY), ida e volta (RETURN) ou é composta de múltiplos destinos (MULTI-CITY).
Deve retornar com body vazio com um dos códigos a seguir:
- 201: em caso de sucesso.
- 400: caso o JSON seja inválido.
- 422: se qualquer um dos campos não for parseável ou se a data de início da viagem é mais recente que a data de fim (para todos os casos, exceto viagens apenas de ida).
PUT/api-travels/travels/{id}: atualiza uma viagem.
Body da requisição:
{
"id": 1,
"orderNumber": "220788",
"amount": "30.06",
"startDate": "2019–09–11T09:59:51.312Z",
"type": "ONE-WAY"
}
Deve ser enviado o objeto que será modificado. O retorno deve ser o próprio objeto modificado.
{
"id": 1,
"orderNumber": "220788",
"amount": "30.06",
"startDate": "2019–09–11T09:59:51.312Z",
"type": "ONE-WAY"
}
A resposta deve conter os códigos a seguir:
- 200: em caso de sucesso.
- 400: caso o JSON seja inválido.
- 404: caso tentem atualizar um registro que não existe.
- 422: se qualquer um dos campos não for parseável (JSON mal formatado).
GET/api-travels/travels: retorna todas as viagens criadas.
Deve retornar uma lista de viagens.
{
"id": 1,
"orderNumber": "220788",
"amount": "30.06",
"startDate": "2019–09–11T09:59:51.312Z",
"type": "ONE-WAY"
},
{
"id": 2,
"nsu": "300691",
"amount": "120.0",
"startDate": "2019–09–11T10:22:30.312Z",
"type": "ONE-WAY"
}
A resposta deve conter os códigos a seguir:
- 200: caso exista viagens criadas.
- 404: caso não exista viagens criadas.
DELETE/api-travels/travels: remove todas as viagens.
Deve aceitar uma requisição com body vazio e retornar 204.
GET/api-travels/statistics: retorna estatísticas básicas sobre as viagens criadas.
{
"sum": "150.06",
"avg": "75.3",
"max": "120.0",
"min": "30.06",
"count": "2"
}
Em que:
- sum: um BigDecimal especificando a soma total das viagens criadas.
- avg: um BigDecimal especificando a média dos valores das viagens criadas.
- max: um BigDecimal especificando o maior valor dentre as viagens criadas.
- min: um BigDecimal especificando o menor valor dentre as viagens criadas.
- count: um long especificando o número total de viagens.
Todos os campos que são BigDecimal devem ter apenas duas casas decimais, por exemplo: 15.385 deve ser retornado como 15.39.
Detalhadas as funcionalidades que precisamos implementar, mãos à obra!
1) Primeiro passo é criar um projeto Spring Boot no Spring Initializr.
Como dependências, vamos selecionar o Spring Web (Spring MVC) e Lombok. O Spring MVC é um framework que ajuda no desenvolvimento de aplicações web no padrão MVC (model-view-controller). O Lombok é uma biblioteca Java focada em produtividade e redução de código por meio de anotações que ensinam o compilador a criar e manipular código Java. Ou seja, você não vai mais precisar escrever métodos getter, setter, equals, hashCode, construtores de classe, etc.
2) Com o projeto criado, vamos criar os pacotes model, controller e service.
Em seguida, criaremos as models: Travel
e Statistic
.
3) Com as models criadas, criaremos as services com as funcionalidades da API: TravelService
e StatisticService
.
A classe TravelService
deve conter sete métodos para atender os requisitos do ciclo de vida desse serviço na API: isJSONValid
(para verificar se o JSON é válido), parseId
(para parsear o campo id do JSON), parseAmount
(para parsear o campo amount do JSON), parseStartDate
(para parsear o campo startDate do JSON), parseEndDate
(para parsear o campo endDate do JSON), isStartDateGreaterThanEndDate
(para verificar se a data de início da viagem é anterior a data de fim da viagem), create
(para criar uma viagem), update
(atualizar uma viagem), add
(para adicionar uma viagem na lista), findById
(para recuperar uma viagem por id), find
(para recuperar todas as viagens criadas) e delete
(remover viagens).
Já a classe StatisticService
deve conter apenas o método para criar estatísticas da API: createStatistics
.
4) Com as funcionalidades criadas, faremos agora as rotas nas controllers: TravelController
(para as rotas relacionadas às viagens) e StatisticController
(para as rotas relacionadas às estatísticas).
Na classe TravelController
, implementaremos as operações detalhadas no início da seção: criar uma viagem (POST), atualizar uma viagem (PUT), listar todas as viagens (GET) e remover todas as viagens (DELETE).
Criadas as rotas, vamos subir a API com o comando:
mvn spring-boot:run
Também é possível executar a API pela sua IDE como Java Application. O resultado de ambos comandos deve ser semelhante à figura abaixo.
Por default, a API estará disponível no endereço: http://localhost:8080/. Com a API funcionando já podemos testar o funcionamento das rotas. Para os testes iniciais, utilizei o Postman e como referência os exemplos do enunciado da seção Construindo nossa API.
POST/api-travels/travels via Postman
PUT/api-travels/travels/1 via Postman
GET/api-travels/travels via Postman
DELETE/api-travels/travels via Postman
Na classe StatisticController
, implementaremos a operação e cálculo das estatísticas detalhada no início da seção.
GET/api-travels/statistics via Postman
Executando testes unitários e de integração
Como testar sua API de forma simples e rápida
Com as funcionalidades da API implementadas, precisamos testá-las. Para automatizar os testes da aplicação utilizei o JUnit5. Para que ambos os testes (unitário e integração) sejam executados corretamente (nos goals corretos do Maven), precisamos adicionar alguns plugins no nosso pom.xml
: maven-failsafe-plugin e build-helper-maven-plugin. A configuração deve ser a mesma abaixo:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<trimStackTrace>false</trimStackTrace>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId
<executions>
<execution>
<id>add-integration-test-sources</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>src/it/java</source>
</sources>
</configuration>
</execution>
<execution>
<id>add-integration-test-resources</id>
<phase>generate-test-resources</phase>
<goals>
<goal>add-test-resource</goal>
</goals>
<configuration>
<resources>
<resource>
<directory>src/it/resources</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
Conforme já estruturado na configuração, criaremos dois pacotes: src/test/java
, de teste unitário, que irá conter métodos para validar a lógica implementada nas services e controllers; e src/it/java
para classe TravelsJavaApiIntegrationTest
, de teste de integração para validar integração e fluxo das rotas na API. Para os testes de integração, utilizaremos o TestRestTemplate do próprio Spring Boot.
Caso você não tenha familiarização com JUnit, leia esse artigo. Para executar somente os testes unitários use o comando:
mvn test
Para executar os testes unitários e de integração, basta usar o comando:
mvn integration-test
O resultado deve ser semelhante ao da figura abaixo:
Com o fluxo de teste pronto, o último passo é criar o fluxo de integração contínua: fazer com que a evolução da API seja monitorada constantemente, mantendo a integração frequente do código que foi e será desenvolvido. Usaremos como ferramenta o TravisCI. Para mais detalhes da prática e do TravisCI, leia esse artigo. Na próxima seção, faremos a configuração final.
Configurando Integração Contínua com TravisCI
Construir sempre para conquistar sempre!
O primeiro passo para habilitar o fluxo da integração contínua é criar um arquivo .travis.yml, na raiz do projeto, com a seguinte configuração:
Nessa configuração, estabelecemos que o build será feito em uma distribuição Linux mais atual, com a linguagem Java, sem permissão de admin e com o JDK 11. Com a máquina virtual configurada, o TravisCI irá fazer o download do código em até 2 commits, executar a instalação do pacote e em seguida os testes.
Em seguida, é necessário habilitar o fluxo da API no dashboard do TravisCI:
Caso a configuração esteja correta, assim que um commit for realizado no projeto, um build da branch principal é disparado automaticamente.
O resultado esperado é que o build seja bem sucedido.
Ao analisar com detalhes o log do build, podemos ver que a branch foi construída e os testes executados com sucesso.
Pronto, nossa API está pronta e validada! O assunto abordado aqui foi de caráter introdutório. Caso tenha propostas de melhoria, é só dar um fork no projeto travels-api no Github e codar!
Se quiser validar algumas boas práticas recomendadas no desenvolvimento de uma API (que será assunto para um outro post), como API versioning, HATEOAS, tratamentos de erros com Spring, dê uma olhada no projeto travels-java-api. Caso tenha outras dúvidas, é só entrar em contato comigo.
Espero que tenham gostado do post. Abraços!
Referências
- HTTP — Hypertext Transfer Protocol (2019): https://pt.wikipedia.org/wiki/Hypertext_Transfer_Protocol
- REST (2019): https://pt.wikipedia.org/wiki/REST
- O que é API? REST e RESTful? Conheça as definições e diferenças! https://becode.com.br/o-que-e-api-rest-e-restful/
- Uma rápida introdução ao REST (2008): https://www.infoq.com/br/articles/rest-introduction/
- Serviços RESTful: verbos HTTP (2019): https://www.devmedia.com.br/servicos-restful-verbos-http/37103
- Status Code Definition (2019): https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
- Qualidade e JUnit: introduzindo automatização de testes unitários do seu software Java no dia-a-dia (2018): https://bit.ly/2m5nOqR.
- Utilizando Travis CI como servidor de integração contínua em projetos open-source: um exemplo com a linguagem Java (2018): https://bit.ly/2kDpI1F