API REST usando Quarkus e Panache

Manollo Guedes
13 min readFeb 16, 2022

--

Quarkus REST API

Você também pode ler a versão em Inglês deste artigo nesse link.

Quarkus é um framework Java que traz em sua essência a ideia de melhorar a utilização de recursos por parte das aplicações. Para que isso aconteça, as aplicações Quarkus são otimizadas principalmente para ambientes serverless, conteinerizados e em nuvem.
Este artigo pretende ser um guia rápido para uma primeira API REST usando Quarkus e Panache, além de algumas outras bibliotecas úteis do Java.

Enjoy 🚀

Antes de mais nada, o que pretendemos construir aqui?

A ideia deste artigo é construir uma API básica para o gerenciamento de produtos.

Após trabalharmos nesse projeto simples mas direto, iremos aprender a:

  • gerar projetos usando Quarkus UI
  • construir uma API REST utilizando RESTEasy
  • Persistir a gerenciar dados utilizando Hibernate e Panache

Além disso, ao final do artigo vamos falar um pouco sobre o padrão de projeto Active Records e como utilizar o Panache para desenvolver models auto persistíveis seguindo esse padrão. Encare isso como um bônus 😉.

Você encontra a versão final do nosso projeto nesse repositório do GitHub.

Gerando o Projeto

Assim como o Spring possui o Spring Initializer para a geração do projeto, Quarkus também conta com uma ferramenta muito útil para a inicialização das nossas aplicações. Você pode acessar essa ferramenta para gerar o seu projeto utilizando esse link.

Captura de tela da ferramenta de criação de projetos do Quarkus
Plataforma para inicialização de projetos Quarkus

Com essa ferramenta você é capaz de adicionar ao seu projeto quantas extensões Quarkus você precisar.

Para esse projeto precisaremos adicionar apenas algumas.

A seguir temos a lista das extensões que serão necessárias para a construção do nosso projeto. Nas seções seguintes discutiremos mais detalhadamente qual o papel de cada uma dessas extensões dentro do nosso projeto, não se preocupe 😃.

  • quarkus-resteasy-jackson
  • quarkus-hibernate-orm-panache
  • quarkus-jdbc-h2

Após adicionar essas extensões será necessário gerar o projeto clicando no botão Generate your application e importar o código gerado na sua IDE favorita (eu estou utilizando o IntelliJ).

Criando nosso model de produtos

Chega de conversa, vamos trabalhar!

O único resource que teremos no nosso projeto serão produtos.

Dessa forma, para começarmos o desenvolvimento vamos criar um novo package chamado org.acme.model. Dentro desse pacote, adicionaremos nossa classe Product com o código a seguir:

Product model

Essa classe contém algumas anotações que talvez você esteja se perguntando

— "De onde saiu essa bagaça?"

Bom, além das extensões que adicionamos anteriormente, utilizaremos uma biblioteca Java chamada Lombok. Utilizando essa biblioteca nós conseguimos simplificar o desenvolvimento, evitando a criação de vários métodos como getters, setters, construtores e vários outros.

Para adicionar essa biblioteca ao nosso projeto precisaremos adicionar uma nova dependência ao nosso pom.xml:

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>

Abaixo temos uma explicação rápida sobre cada uma das anotações que estamos usando no nosso model Product. Você pode consultar todas as anotações providenciadas pelo Lombok acessando este site.

  • Data: gera os métodos getters, setters, toString, equals, hashCode e também cria um construtor utilizando os atributos marcados como final.
  • Builder: gera todo o código necessário para utilizarmos o padrão de projeto builder. Ao setarmos a propriedade toBuilder como true, garantimos também que será gerado um método toBuilder() que nos permita criar um builder a partir de um objeto já construído.
  • AllArgsConstructor e NoArgsConstructor: como os nomes sugerem, ambas anotações são usadas para definir construtores. A primeira gera um construtor que recebe todos os atributos da classe como argumento, a segunda gera um construtor vazio.

Observação importante: para utilizar o Lombok talvez você precise adicionar o plugin do Lombok na sua IDE.

Criando os endpoints da API

Para manipular nossos produtos precisaremos criar um novo controller para gerenciar os endpoints da nossa API.

Para isso, vamos criar um novo package org.acme.controller e adicionar nele uma nova classe ProductController.

Com essa nova classe pretendemos ser capazes de ter um CRUD (acrônimo para o criar, ler, atualizar e deletar — do inglês Create, Read, Update e Delete) de produtos.

A primeira versão do nosso controller deve ser algo mais ou menos assim:

ProductController

Note que por enquanto estamos usando apenas dados fake. Nos próximos passos aprenderemos como usar dados vindos do banco de dados.

Aguenta aí ✋

Para trabalhar com APIs REST, Quarkus utiliza a especificação JAX-RS, mais especificamente a implementação RESTEasy.

Anteriormente adicionamos ao nosso projeto a extensão quarkus-resteasy-jackson. Essa extensão dá ao nosso projeto a capacidade de criar uma API REST e, além disso, desserializar os dados que transitam na nossa API (in/out) utilizando a biblioteca Jackson.

— Ok Manollo, chega de falatório, bora voltar pro código! 💻

Como o Quarkus segue a especificação do JAX-RS, precisamos anotar nossa classe ProductController com a anotação @Path, isso faz com que o java comece a mapear as requisições que dêem match com o path que definimos na classe para os métodos da nossa classe.

Nesse caso utilizaremos o @Path para mapear requests utilizando o caminho "/api/v1/product".

Também estamos anotando ProductController com @Produces e @Consumes. Isso define explicitamente o que nossa classe espera receber de uma request e também o que ela irá produzir como resposta. Ambas annotations podem ser utilizadas em classes e métodos.

Mas ei! Estamos utilizando quarkus-resteasy-jackson! Por isso Quarkus já espera consumir e produzir, por padrão, dados JSON. Então no fim das contas nem precisamos utilizar as anotações @Produces e @Consumes. Estamos adicionando aqui mais por ser uma boa prática do que por necessidade 😉.

OPA! Parece que já temos uma API REST testável! 🕺

Para rodar nosso projeto você pode utilizar o comando a seguir:

mvn quarkus:dev

E disparar um dos nossos endpoints pra ver no que dá 🤞.

Bora utilizar o nosso método GET.

curl --location --request GET 'http://localhost:8080/api/v1/product'

Utilizando Panache para persistir dados

Ok, nesse ponto já conseguimos rodar nossa aplicação… Mas continuamos utilizando dados mockados. De que adianta isso? De nada 😕

Mas calma, a partir deste ponto começaremos a utilizar persistência de dados de verdade 👏.

Quarkus utiliza Hibernate ORM como implementação para o JPA. A extensão que adicionamos antes (quarkus-hibernate-orm-panache) tem como foco tornar ainda mais fácil o gerenciamento das entidades que iremos persistir no banco.

Talvez você esteja se perguntando:

— "Beleza Manollo, vamos persistir os dados mas nem temos tabela ainda!"

Utilizando Hibernates não precisamos criar tabelas manualmente e muito menos definir constraints. Pra facilitar a nossa vida podemos utilizar classes chamadas de entities. Quer uma boa notícia? Nosso model Product pode ser transformado exatamente no que nós precisamos de uma entidade: uma definição do formato da tabela para a persistência.

Bora fazer isso então 💪

Configurando nosso projeto para se conectar ao banco de dados

Antes de criarmos nossa entidade, precisamos definir como nos conectaremos ao banco de dados.

Você deve ter percebido que quando rodamos nossa aplicação nós passamos pro maven a flag dev pra ser usada no Quarkus:

mvn quarkus:dev

Isso faz com que a nossa aplicação seja inicializada em dev mode.

O dev mode do Quarkus é um conjunto de funcionalidades que facilitam a nossa vida quando estamos rodando aplicações em ambiente de desenvolvimento. Um dos benefícios que esse modo de execução traz consigo é a capacidade de desenvolver a aplicação sem nos preocuparmos com as configurações de banco de dados.

— Hã?!

É, você entendeu certo!

Se você está executando seu projeto em modo de desenvolvimento, as configurações de banco de dados não precisam ser uma preocupação sua, o Quarkus é capaz de cuidar disso sozinho. A única coisa que você precisa fazer (ou não fazer no caso) é não informar nenhuma URL de conexão com o banco (quarkus.datasource.jdbc.url), nenhum usuário (quarkus.datasource.username) e nem senha (quarkus.datasource.password) no seu application.properties.

Deixando essas propriedades vazias o Quarkus vai compreender que você quer que ele cuide do banco por você.

A única coisa que é preciso configurar é o tipo de banco de dados pretendemos utilizar. Isso pode ser feito através da propriedade quarkus.datasource.db-kind.

Quarkus suporta uma lista de bancos de dados open source. Você pode consultar a lista completa na Documentação de Datasources do Quarkus. Nesse artigo iremos utilizar o banco de dados H2.

Nota rápida: se você pretende que o Quarkus gerencie a conexão com o banco por você, mas pretende utilizar qualquer outro banco de dados que não seja o H2, será necessário que você suba um Docker Container antes.

Para trabalhar com o H2 nós adicionamos a extensão quarkus-jdbc-h2.

Como dito anteriormente, a única configuração que precisamos fazer é dizer ao Quarkus o tipo de banco de dados que iremos utilizar. Pra isso é preciso adicionar essa propriedade no nosso application.properties.

quarkus.datasource.db-kind=h2

Com essa simples configuração já podemos começar a trabalhar no nosso código.

Transformando nosso model Product em uma Entidade

Pra fazermos isso precisamos dizer ao Hibernate que nossa classe Product é uma entidade.

Como?

Basta anotar a classe com @Entity, isso irá permitir que nossa classe seja utilizada em queries e também que o Hibernate utilize-a para definir a tabela que irá armazenar os produtos.

Se você quiser definir alguns detalhes mais específicos da tabela como nome, schema que será utilizado, e coisas do tipo, é possível utilizarmos em conjunto com @Entity a anotação @Table. No nosso exemplo mudaremos o nome da tabela de Product (primeira letra maiúscula) para product.

Uma informação importante que toda tabela precisa ter é a primary key. Para definir um dos atributos da classe como primary key precisamos anotar a propriedade escolhida como @Id. Utilizaremos o atributo id como id da classe (poxa vida, que surpresa hem?!). Além disso, se quisermos que o próprio Hibernates gerencie a criação dos ids utilizando suas próprias sequences, podemos anotar a nossa propriedade com @GeneratedValue. Caso contrário precisaremos gerar esse id no lado da aplicação.

Temos também uma outra anotação muito útil, @Column. Assim como a anotação @Table nos permite definirmos detalhes específicos à tabela, a anotação @Column nos permite fazer o mesmo para a coluna, definindo detalhes como nullability, precisão, tamanho e outras coisas.

Ok, com tudo isso dito, a versão final da nossa entidade Produto deve ser algo assim:

Product class with entity related annotations

Criando um Repository utilizando Panache

Conforme dito anteriormente, Panache implementa o Hibernate ORM e nos traz algumas facilidades no gerenciamento das entidades. Para isso, Panache nos apresenta um cinto de utilidades com diversos métodos de manipulação como findById, list, update, persist e delete.

Para utilizar esses métodos precisaremos criar uma nova classe chamada ProductRepository dentro de um novo package org.acme.repository.

Essa nova classe precisará implementar PanacheRepository<Product> para ter acesso aos métodos.

A primeira aparência da nossa classe será algo assim:

ProductRepository

Pronto, com esse código simples já temos acesso a todos os métodos providenciados pelo Panache. Só precisamos importar essa nova classe e começar a utilizá-la…

Mas nem tudo são flores haha, para o nosso cenário precisaremos ir além e criar alguns métodos novos!

Quando definimos nossos endpoints na classe ProductController você deve ter notado que, por exemplo, temos um método que recebe o nome de um produto e sua marca, e precisa retornar os produtos relacionados a esses nomes…

Como poderíamos fazer isso utilizando somente os métodos default do Panache? Bom, se não queremos trazer o banco de dados inteiro pro nosso código e filtrar tudo isso usando puramente Java, não podemos… E acredite, a ideia de trazer tudo para o Java é terrível então não vá por esse caminho haha

Panache tem muitos métodos que são uma mão na roda pra nós. Mas em algumas ocasiões precisaremos criar novos métodos, e tá tudo bem, isso não é difícil de ser feito. Você vai ver 😃

Bora criar então mais 3 métodos na nossa classe ProductRepository: findByName, findByBrand e findByNameAndBrand.

ProductRepository findBy methods

Pronto, com esses métodos já somos capazes de utilizar todos os métodos GET que definimos como endpoint.

Mas você concorda comigo que se estamos procurando um produto pelo seu nome, não deveríamos nos preocupar se o nome está em maiúsculo ou minúsculo? Para isso podemos tratar os dados de duas formas, pelo menos. Podemos pré formatar os dados no momento de salvarmos os dados no banco de dados, ou converter os dados enquanto fazemos a consulta.

Ambas opções são fáceis de serem implementadas, mas a segunda opção pode afetar (e muito) a performance do banco de dados. Então vamos utilizar a primeira opção. 😄

Pra tratarmos os dados no momento de salvarmos precisaremos criar nosso próprio método de persistência que irá receber uma instância de Product, capitalizar seu nome e marca e irá salvar os dados no banco.

Pra isso utilizaremos uma nova dependência, apache commons-text, essa biblioteca possui métodos utilitários muito interessantes para trabalharmos com texto.

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>

Nosso método de persistência ficará mais ou menos assim:

ProductRepository persistence method overload

Você pode ver que mesmo que estejamos sobrecarregando o método persist, estamos utilizando sua implementação default providenciada pelo Panache para salvarmos os dados ao final da formatação dos dados.

A escolha por formatar os dados no repository e não numa camada de service se deve ao fato de que assim garantimos que todo e qualquer código que chamar o método de persistência passando o produto irá passar antes pela formatação dos dados.

Por último mas não menos importante, precisaremos criar um método de update que irá buscar por um produto e atualizar os dados tendo como base o seu id.

ProductRepository method to update products

O Panache tem um método update padrão, porém o método utiliza uma query para fazer a atualização dos dados. É mais fácil trabalhar com essa atualização utilizando Java no lugar de HQL ou JPQL. Por isso criamos esse método.

Ok… Agora que já criamos tudo relacionado a manipulação de dados precisamos fazer uma escolha: ter uma camada de serviço para regras de negócio, ou fazer tudo isso entre controller e repository? Eu particularmente gosto de ter uma camada de serviço para validação de dados e outras regras de negócio que venham a ser necessárias. Mas se você não quiser utilizar a camada de serviço, basta fazer a chamada do repository diretamente pelo controller.

Camada de Serviço para validação de dados e regras de negócio

Nossa nova classe de serviço, ProductService, será colocada num novo package org.acme.service. Essa classe será utilizada para acessarmos nosso repository e aplicarmos regras de validação e manipulação de dados.

Para fazer isso precisaremos injetar o bean do nosso repository em ProductService.

— Tá bom Manollo, como que eu vou fazer isso?

É bem simples!

Quarkus trabalha com CDI (context and dependency injection), trocando em miúdos (muito grossamente), isso significa que o Quarkus se encarrega de descobrir beans dentro do nosso projeto e injetá-los nas classes e contextos que o utilizam.

Nota rápida: CDI é um assunto bastante complexo, se quiser ler mais sobre o assunto, recomendo a documentação oficial do Quarkus sobre CDI e a documentação oficial do Jakarta para CDI em Java.

Voltando ao nosso projeto, se você olhar o código que colocamos no nosso ProductRepository vai ver que anotamos a classe como @ApplicationScoped, isso assinala a nossa classe como sendo um bean aplicável no escopo do ciclo de vida completo da aplicação. Ou seja, desde sua inicialização até a sua destruição. Desse modo, qualquer construtor que tenha como parâmetro uma instância de ProductRepository poderá se beneficiar dessa característica, não sendo necessária a instanciação manual do repository, sendo o Quarkus o responsável por criar a instância do bean e disponibilizá-la ao construtor.

Tá bom, mas então isso quer dizer que nós precisaremos ter um construtor na nossa classe ProductService, certo? Exato, mas não precisaremos criá-lo manualmente, temos o Lombok pra fazer isso pra nós 👏. Basta anotar ProductService com @AllArgsConstructor. Isso fará o Lombok criar um construtor que receba todos os atributos da classe como parâmetro, portanto receberá a injeção de ProductRepository 🕺

Essa é a versão final da nossa classe de serviço.

ProductService

Na classe temos todos os métodos que serão necessários na ProductController.

Como você pode ver, os métodos create, replace e delete estão anotados como @Transactional.

— O que isso significa?

Quando temos métodos que fazem modificações no banco de dados é necessário que controlemos a transação em que essas alterações serão feitas. Pra isso podemos tanto fazer o controle manual, quanto pedir ao próprio Hibernates pra cuidar disso pra nós. No nosso projeto pediremos ao Hibernate pra tomar conta disso, afinal de contas nós temos mais o que fazer né não?! kkk

@Transactional faz exatamente isso, informa ao Hibernates que ele precisa providenciar tudo o que for necessário pra que a transação aconteça corretamente, fazendo commit ou dando rollback conforme necessário.

Atualizando ProductController para chamar o classe de serviço

Agora o que precisamos fazer é basicamente usar a nossa classe de serviço no lugar das chamadas fake que colocamos no início desse projeto.

Ao fazer isso, nosso código ficará mais ou menos assim:

Bom, se você chegou aqui você já deve ter um código completamente funcional e pronto pra rodar :D parabéns! 👏

Se tiver qualquer problema com seu código, você pode conferir a versão final do projeto nesse repositório do GitHub.

Bônus Final: Active Record

Active Record é um padrão de projeto usado para empoderar as classes de entidades. Quando seguimos este padrão, basicamente tornamos dispensáveis as classes repository, uma vez que tornamos as entidades responsáveis por cuidar não somente do comportamente mas também dos dados.

Martin Fowler descreve uma entidade que segue esse padrão como sendo (em tradução livre):

Um wrap para uma linha de uma tabela ou view no banco de dados, encapsulando o acesso ao banco de dados e adicionando lógica de domínio a esses dados.

Para dispensar a necessidade de termos uma outra classe cuidando do acesso aos dados, uma entidade usando Active Records precisa providenciar métodos de acesso e manipulação de dados.

Usando Panache conseguimos fazer tudo o que precisamos para facilmente transformar nossa classe Product em um Active Record. Pra fazermos isso basta que extendamos a classe PanacheEntity.

Boooooom! Fazendo isso já não precisaremos mais de um ProductRepository!

Classe Product usando Active Record

A partir de agora nossa classe Product não só representa os dados de um produto mas também é capaz de manipular esses mesmos dados usando praticamente os mesmos métodos default que tínhamos antes em ProductRepository como findById, persist e tantos outros.

Claro, assim como em ProductRepository, teremos que adicionar outros métodos que se adequem ao que esperamos do nosso projeto. Deixarei isso como exercício pra você 😉

Tudo isso é muito bonito e simples, mas como diria o Tio Ben:

Com grandes poderes vem grandes resposabilidades

Active Records nos permitem acessar dados usando nossas classes de domain, mas fazendo isso este padrão de projeto também traz algumas disvantagens:

  • Alto acoplamento: Active Records essencialmente mistura as camadas de domínio e persistência, tornando essas duas camadas absolutamente amarradas uma na outra.
  • Criar testes se torna quase um pesadelo: como temos um altíssimo acoplamento entre as camadas de domínio e persistência, se torna muito difícil separar as responsabilidades de ambas camadas para que uma seja mockada enquanto a outra é testada.
  • SRP: Single Responsability Principle (ou Princípio da Responsabilidade Única) chora ao ouvir falar de Active Records. Active Records por essência coloca nos ombros de uma única classe multiplas funções, como já vimos. Usando esse padrão, facilmente nossas classes podem se tornar verdadeiros monstrinhos como dezenas de métodos.

Como Software Developers, nossa função não é só escrever código mas pensar no que estamos escrevendo. Faz parte disso analisarmos prós e contras antes de tomarmos qualquer decisão. Usar ou não Active Records não seria diferente 😅

--

--