📚 Série: Quarkus: Desvendando o Desenvolvimento Moderno com Java
Este é o quinto capítulo de uma série completa sobre Quarkus. Prepare-se para uma jornada que vai transformar sua visão sobre desenvolvimento Java moderno!
Você já tem uma aplicação Quarkus funcionando e sabe como gerenciar dependências e configurações. Agora é hora de construir APIs que o mundo pode consumir! 🌍
No desenvolvimento moderno, APIs RESTful são a ponte entre diferentes sistemas - são elas que permitem que sua aplicação mobile converse com o backend, que microsserviços se comuniquem, e que sistemas externos integrem com sua solução.
O Quarkus, com sua integração otimizada com RESTEasy, torna a criação de APIs RESTful uma experiência incrível. Vamos descobrir como! 💪
🎯 O que você vai aprender
- Conceitos fundamentais de REST e JAX-RS
- Como criar endpoints REST no Quarkus
- Tratamento robusto de erros e exceções
- Validação profissional de dados
- Manipulação de dados JSON/XML
- Boas práticas para APIs de produção
🏗️ Fundamentos: REST e JAX-RS Explicados
REST: Mais que um Protocolo, uma Filosofia
REST (Representational State Transfer) não é apenas uma tecnologia - é um estilo arquitetural que define como sistemas distribuídos devem se comunicar.
Os pilares do REST são:
🎯 Recursos como Cidadãos de Primeira Classe
- Tudo é um recurso: usuários, produtos, pedidos
- Cada recurso tem uma URL única:
/users/123
,/products/456
🔄 Stateless por Design
- Cada requisição é independente
- O servidor não "lembra" de requisições anteriores
- Isso facilita escalabilidade e cache
⚡ Verbos HTTP com Significado
-
GET
→ Buscar dados (sem efeitos colaterais) -
POST
→ Criar novos recursos -
PUT
→ Atualizar recursos completos -
DELETE
→ Remover recursos -
PATCH
→ Atualizações parciais
🎨 Múltiplas Representações
- O mesmo recurso pode ser JSON, XML, HTML
- O cliente escolhe o formato via headers
JAX-RS: REST para Desenvolvedores Java
JAX-RS (Java API for RESTful Web Services) é a especificação Java que transforma esses conceitos em código. É como ter um tradutor universal entre HTTP e Java.
// Isso é JAX-RS em ação! 🎉 @GET @Path("/users/{id}") @Produces("application/json") public User getUser(@PathParam("id") Long id) { return findUserById(id); }
O RESTEasy é uma das implementações mais robustas do JAX-RS, e o Quarkus o integra de forma nativa e supersônica! 🚄
🛠️ Mãos à Obra: Criando Nossa API de Produtos
Vamos construir uma API completa para gerenciar produtos. É um exemplo prático que você pode adaptar para qualquer domínio!
💡 Dica Pro: A dependencia quarkus-rest
utiliza o resteasy-reactive
é a versão mais moderna e performática. Ele usa programação reativa por baixo dos panos, mesmo que você escreva código "tradicional"!
Passo 1: O Modelo de Dados
Vamos criar nossa classe Product - simples, mas poderosa:
package com.example.model; public class Product { public Long id; public String name; public String description; public Double price; // Construtor padrão (necessário para JSON) public Product() {} // Construtor completo public Product(Long id, String name, String description, Double price) { this.id = id; this.name = name; this.description = description; this.price = price; } // Método útil para debugging @Override public String toString() { return String.format("Product{id=%d, name='%s', price=%.2f}", id, name, price); } }
🤔 Por que campos públicos? Quarkus foi desenhado para rodar de forma ultrarrápida, tanto no modo JVM quanto nativamente com GraalVM. Evitar reflection pesado, como o que é usado para acessar campos privados via getters/setters, melhora a performance da serialização/desserialização de JSON.
Frameworks como Jackson ou JSON-B conseguem acessar campos públicos diretamente, sem precisar de introspecção extra.
⚠️ Quando não usar campos públicos
Apesar da praticidade, campos públicos não são ideais em todos os cenários:
Quando você precisa de validações personalizadas ou lógica de negócio nos getters/setters.
Quando precisa proteger invariantes de estado da sua entidade.
Quando quer seguir padrões de encapsulamento rigorosos (como em DDD).
✅ Quando usar
Campos públicos são totalmente aceitáveis e até recomendados no Quarkus para:
DTOs
Modelos usados apenas para transporte
Testes e mocks
Classes simples e imutáveis
Passo 2: O Recurso REST - Onde a Mágica Acontece
Agora vem a parte mais interessante - nossa classe de recurso REST:
package com.example.resource; import com.example.model.Product; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; @Path("/products") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ProductResource { // Simulando um banco de dados em memória private static final List<Product> products = new ArrayList<>(); private static final AtomicLong idGenerator = new AtomicLong(); // Dados de exemplo para testar static { products.add(new Product( idGenerator.incrementAndGet(), "MacBook Pro M3", "Laptop profissional para desenvolvimento", 2999.99 )); products.add(new Product( idGenerator.incrementAndGet(), "Mouse MX Master 3", "Mouse ergonômico para produtividade", 89.99 )); products.add(new Product( idGenerator.incrementAndGet(), "Teclado Mecânico RGB", "Teclado para gamers e desenvolvedores", 159.99 )); } @GET public List<Product> getAllProducts() { return products; } @GET @Path("/{id}") public Response getProductById(@PathParam("id") Long id) { Optional<Product> product = products.stream() .filter(p -> p.id == id) .findFirst(); return product .map(p -> Response.ok(p).build()) .orElse(Response.status(Response.Status.NOT_FOUND) .entity("Produto não encontrado") .build()); } @POST public Response createProduct(Product product) { // Validação básica if (product.name == null || product.name.trim().isEmpty()) { return Response.status(Response.Status.BAD_REQUEST) .entity("Nome do produto é obrigatório") .build(); } product.id = idGenerator.incrementAndGet(); products.add(product); return Response.status(Response.Status.CREATED) .entity(product) .build(); } @PUT @Path("/{id}") public Response updateProduct(@PathParam("id") Long id, Product updatedProduct) { Optional<Product> existingProduct = products.stream() .filter(p -> p.id == id) .findFirst(); if (existingProduct.isPresent()) { Product product = existingProduct.get(); product.name = updatedProduct.name; product.description = updatedProduct.description; product.price = updatedProduct.price; return Response.ok(product).build(); } return Response.status(Response.Status.NOT_FOUND) .entity("Produto não encontrado") .build(); } @DELETE @Path("/{id}") public Response deleteProduct(@PathParam("id") Long id) { boolean removed = products.removeIf(p -> p.id == id); if (removed) { return Response.noContent().build(); } return Response.status(Response.Status.NOT_FOUND) .entity("Produto não encontrado") .build(); } }
🎯 Decifrando as Anotações JAX-RS
Cada anotação tem um propósito específico:
-
@Path("/products")
→ Define a URL base (/products
) -
@Produces(APPLICATION_JSON)
→ "Eu falo JSON!" 🗣️ -
@Consumes(APPLICATION_JSON)
→ "Eu entendo JSON!" 👂 -
@GET
,@POST
, etc. → Mapeia métodos HTTP -
@PathParam("id")
→ Extrai valores da URL -
Response
→ Controle total sobre status codes e headers
🧪 Testando Nossa API Básica
Inicie sua aplicação com:
mvn quarkus:dev
Agora você pode testar os endpoints:
📋 Listar todos os produtos
curl http://localhost:8080/products
🔍 Buscar produto específico
curl http://localhost:8080/products/1
➕ Criar novo produto
curl -X POST http://localhost:8080/products \ -H "Content-Type: application/json" \ -d '{ "name": "Monitor 4K", "description": "Monitor ultrawide para programação", "price": 799.99 }'
✏️ Atualizar produto
curl -X PUT http://localhost:8080/products/1 \ -H "Content-Type: application/json" \ -d '{ "name": "MacBook Pro M3 - Atualizado", "description": "Versão atualizada com mais RAM", "price": 3299.99 }'
🗑️ Deletar produto
curl -X DELETE http://localhost:8080/products/1
⚠️ Tratamento de Erros e Exceções
Agora que temos nossa API básica funcionando, precisamos torná-la robusta com um sistema profissional de tratamento de erros. Uma API robusta deve lidar graciosamente com situações inesperadas.
1. Exceções Personalizadas
Crie exceções específicas para seu domínio:
package com.example.exception; public class ProductNotFoundException extends RuntimeException { public ProductNotFoundException(Long id) { super("Produto com ID " + id + " não foi encontrado"); } }
package com.example.exception; public class InvalidProductDataException extends RuntimeException { public InvalidProductDataException(String message) { super(message); } }
2. Classe de Resposta de Erro Padronizada
package com.example.exception; import java.time.LocalDateTime; public class ErrorResponse { public String code; public String message; public LocalDateTime timestamp; public String path; public ErrorResponse(String code, String message) { this.code = code; this.message = message; this.timestamp = LocalDateTime.now(); } public ErrorResponse(String code, String message, String path) { this(code, message); this.path = path; } }
3. Mapeadores de Exceção Globais
package com.example.exception; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.ExceptionMapper; import jakarta.ws.rs.ext.Provider; @Provider public class ProductNotFoundExceptionMapper implements ExceptionMapper<ProductNotFoundException> { @Override public Response toResponse(ProductNotFoundException exception) { return Response.status(Response.Status.NOT_FOUND) .entity(new ErrorResponse("PRODUCT_NOT_FOUND", exception.getMessage())) .build(); } }
package com.example.exception; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.ExceptionMapper; import jakarta.ws.rs.ext.Provider; @Provider public class GeneralExceptionMapper implements ExceptionMapper<RuntimeException> { @Override public Response toResponse(RuntimeException exception) { // Log da exceção para monitoramento System.err.println("Erro interno: " + exception.getMessage()); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(new ErrorResponse("INTERNAL_ERROR", "Erro interno do servidor")) .build(); } }
4. Usando as Exceções no Resource
Agora podemos atualizar nosso resource para usar o sistema de tratamento de erros:
@GET @Path("/{id}") public Product getProductById(@PathParam("id") Long id) { return products.stream() .filter(p -> p.id.equals(id)) .findFirst() .orElseThrow(() -> new ProductNotFoundException(id)); } @POST public Response createProduct(Product product) { // Validação de negócio usando nossa exceção personalizada if (products.stream().anyMatch(p -> p.name.equals(product.name))) { throw new InvalidProductDataException("Já existe um produto com este nome"); } product.id = idGenerator.incrementAndGet(); products.add(product); return Response.status(Response.Status.CREATED).entity(product).build(); }
Agora nossa API tem um sistema de tratamento de erros consistente e profissional! 🛡️
🛡️ Validação de Dados: Sua Primeira Linha de Defesa
Com o tratamento de erros implementado, podemos adicionar validação robusta que usa essas estruturas. APIs robustas nunca confiam nos dados que recebem!
Adicionando Bean Validation
Primeiro, adicione a extensão de validação:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-hibernate-validator</artifactId> </dependency>
Modelo com Validações
Atualize sua classe Product:
package com.example.model; import jakarta.validation.constraints.*; public class Product { public Long id; @NotBlank(message = "Nome é obrigatório") @Size(min = 2, max = 100, message = "Nome deve ter entre 2 e 100 caracteres") public String name; @Size(max = 500, message = "Descrição não pode exceder 500 caracteres") public String description; @NotNull(message = "Preço é obrigatório") @DecimalMin(value = "0.0", inclusive = false, message = "Preço deve ser maior que zero") @Digits(integer = 10, fraction = 2, message = "Preço deve ter no máximo 2 casas decimais") public Double price; public Product() {} public Product(Long id, String name, String description, Double price) { this.id = id; this.name = name; this.description = description; this.price = price; } }
Ativando Validação nos Endpoints
Adicione @Valid
nos métodos que recebem dados:
@POST public Response createProduct(@Valid Product product) { product.id = idGenerator.incrementAndGet(); products.add(product); return Response.status(Response.Status.CREATED).entity(product).build(); } @PUT @Path("/{id}") public Response updateProduct(@PathParam("id") Long id, @Valid Product updatedProduct) { // ... resto do código }
Personalizando Mensagens de Validação
Para ter controle total sobre as mensagens, você pode criar um tratador de exceções de validação que usa nossa estrutura de erro:
package com.example.exception; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.ExceptionMapper; import jakarta.ws.rs.ext.Provider; import java.util.Set; import java.util.stream.Collectors; @Provider public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> { @Override public Response toResponse(ConstraintViolationException exception) { Set<ConstraintViolation<?>> violations = exception.getConstraintViolations(); String errors = violations.stream() .map(violation -> { // Extrai o último nome do caminho da propriedade String path = violation.getPropertyPath().toString(); String field = path.contains(".") ? path.substring(path.lastIndexOf('.') + 1) : path; return String.format("%s: %s", field, violation.getMessage()); }) .distinct() .collect(Collectors.joining(", ")); return Response.status(Response.Status.BAD_REQUEST) .entity(new ErrorResponse("VALIDATION_ERROR", "Dados inválidos: " + errors)) .build(); } }
Testando Validação
Tente criar um produto inválido:
curl -X POST http://localhost:8080/products \ -H "Content-Type: application/json" \ -d '{ "name": "", "price": -10 }'
Resposta esperada (Status: 400 Bad Request):
{ "code":"VALIDATION_ERROR", "message":"Dados inválidos: price: Preço deve ser maior que zero, name: Nome deve ter entre 2 e 100 caracteres, name: Nome é obrigatório", "timestamp":"2025-06-17T21:54:22.5735566" }
Agora nossa API tem validação consistente usando nosso sistema de tratamento de erros! 🎨
🎨 Suporte a Múltiplos Formatos
Embora JSON seja o padrão, às vezes você precisa suportar XML ou outros formatos. Com nossa base sólida de tratamento de erros e validação, podemos facilmente adicionar múltiplos formatos:
Para XML
Adicione a dependência JAXB:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-rest-jaxb</artifactId> </dependency>
Crie uma classe wrapper anotada com @XmlRootElement para encapsular a lista de produtos
package com.example.model; import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlRootElement; import java.util.List; @XmlRootElement(name = "products") public class ProductList { private List<Product> products; public ProductList() { // necessário para JAXB } public ProductList(List<Product> products) { this.products = products; } @XmlElement(name = "product") public List<Product> getProducts() { return products; } public void setProducts(List<Product> products) { this.products = products; } }
Adicione anotações JAXB no Modelo
package com.example.model; import jakarta.xml.bind.annotation.XmlRootElement; import jakarta.validation.constraints.*; @XmlRootElement // Necessário para XML public class Product { public Long id; @NotBlank(message = "Nome é obrigatório") @Size(min = 2, max = 100, message = "Nome deve ter entre 2 e 100 caracteres") public String name; @Size(max = 500, message = "Descrição não pode exceder 500 caracteres") public String description; @NotNull(message = "Preço é obrigatório") @DecimalMin(value = "0.0", inclusive = false, message = "Preço deve ser maior que zero") public Double price; // ... construtores }
Atualize as Anotações do Resource
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public class ProductResource { @GET public ProductList getAllProducts() { return new ProductList(products); } // ... os outros métodos permanecem iguais }
Testando XML
Receber resposta em XML:
curl -H "Accept: application/xml" http://localhost:8080/products/1
Resposta esperada:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <product> <id>1</id> <name>MacBook Pro M3</name> <description>Laptop profissional para desenvolvimento</description> <price>2999.99</price> </product>
Enviar dados em XML:
curl -X POST http://localhost:8080/products \ -H "Content-Type: application/xml" \ -d '<?xml version="1.0" encoding="UTF-8"?> <product> <name>Produto via XML</name> <description>Criado usando XML</description> <price>99.99</price> </product>'
🎭 Negociação de Conteúdo Automática
O RESTEasy faz negociação de conteúdo automaticamente baseada nos headers:
-
Accept: application/json
→ Resposta em JSON -
Accept: application/xml
→ Resposta em XML -
Accept: */*
→ JSON (padrão) -
Content-Type: application/json
→ Lê JSON -
Content-Type: application/xml
→ Lê XML
Outros Formatos Suportados
Para YAML (adicione quarkus-resteasy-reactive-yaml
):
@Produces({MediaType.APPLICATION_JSON, "application/yaml"})
Para texto simples:
@GET @Path("/info") @Produces(MediaType.TEXT_PLAIN) public String getInfo() { return "API de Produtos v1.0 - Total de produtos: " + products.size(); }
🚀 Dicas Pro para APIs de Produção
Com nossa base sólida (CRUD + tratamento de erros + validação + múltiplos formatos), podemos implementar recursos avançados para produção:
1. Paginação para Listas Grandes
Quando você tem milhares de produtos, retornar todos de uma vez não é viável:
@GET public Response getAllProducts( @QueryParam("page") @DefaultValue("0") int page, @QueryParam("size") @DefaultValue("10") int size, @QueryParam("sort") @DefaultValue("id") String sortBy) { // Validação básica if (page < 0 || size < 1 || size > 100) { return Response.status(Response.Status.BAD_REQUEST) .entity(new ErrorResponse("INVALID_PARAMS", "Parâmetros de paginação inválidos")) .build(); } // Simulando paginação (em um cenário real, isso viria do banco) int startIndex = page * size; int endIndex = Math.min(startIndex + size, products.size()); List<Product> pageProducts = products.subList(startIndex, endIndex); return Response.ok(pageProducts) .header("X-Total-Count", products.size()) .header("X-Page", page) .header("X-Page-Size", size) .build(); }
Testando paginação:
# Primeira página (produtos 0-2) curl "http://localhost:8080/products?page=0&size=2" # Segunda página (produto 3) curl "http://localhost:8080/products?page=1&size=2"
2. Filtros e Busca
Permita que os usuários encontrem o que procuram:
@GET @Path("/search") public Response searchProducts( @QueryParam("name") String name, @QueryParam("minPrice") Double minPrice, @QueryParam("maxPrice") Double maxPrice) { return Response.ok(products.stream() .filter(p -> name == null || p.name.toLowerCase().contains(name.toLowerCase())) .filter(p -> minPrice == null || p.price >= minPrice) .filter(p -> maxPrice == null || p.price <= maxPrice) .collect(Collectors.toList())) .build(); }
Exemplos de uso:
# Buscar produtos com "mac" no nome curl "http://localhost:8080/products/search?name=mac" # Produtos entre R$ 50 e R$ 200 curl "http://localhost:8080/products/search?minPrice=50&maxPrice=200" # Combinar filtros curl "http://localhost:8080/products/search?name=mouse&maxPrice=100"
3. Headers Úteis para Performance
@GET @Path("/{id}") public Response getProductById(@PathParam("id") Long id) { Optional<Product> product = findProductById(id); if (product.isPresent()) { return Response.ok(product.get()) // Cache por 5 minutos .header("Cache-Control", "max-age=300") // ETag para versionamento .header("ETag", "\"" + product.get().hashCode() + "\"") // CORS headers .header("Access-Control-Allow-Origin", "*") .build(); } throw new ProductNotFoundException(id); }
4. Documentação com OpenAPI/Swagger
Adicione documentação automática:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-openapi</artifactId> </dependency>
Anote seus endpoints se quiser personalizar:
import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.responses.ApiResponse; import org.eclipse.microprofile.openapi.annotations.tags.Tag; @Tag(name = "Produtos", description = "Operações relacionadas a produtos") @Path("/products") public class ProductResource { @GET @Operation(summary = "Listar todos os produtos", description = "Retorna uma lista de todos os produtos disponíveis") @APIResponse(responseCode = "200", description = "Lista de produtos retornada com sucesso") public List<Product> getAllProducts() { return products; } @POST @Operation(summary = "Criar novo produto", description = "Cria um novo produto com os dados fornecidos") @APIResponse(responseCode = "201", description = "Produto criado com sucesso") @APIResponse(responseCode = "400", description = "Dados inválidos fornecidos") public Response createProduct(@Valid Product product) { // ... implementação } }
Acesse a documentação:
- Swagger UI: http://localhost:8080/q/swagger-ui/
- OpenAPI JSON: http://localhost:8080/q/openapi
5. Versionamento de API
Quando sua API evolui, você precisa manter compatibilidade:
Opção 1: Versionamento por URL
@Path("/v1/products") public class ProductResourceV1 { // Versão antiga da API } @Path("/v2/products") public class ProductResourceV2 { // Nova versão com recursos adicionais }
Opção 2: Versionamento por Header
@GET @Path("/products") public Response getProducts(@HeaderParam("API-Version") String version) { if ("v2".equals(version)) { // Lógica da v2 return Response.ok(enhancedProducts).build(); } // Lógica padrão (v1) return Response.ok(products).build(); }
6. Rate Limiting Básico
Para APIs públicas, implemente limitação de taxa:
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @Path("/products") public class ProductResource { private final Map<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>(); private final int MAX_REQUESTS_PER_MINUTE = 100; @GET public Response getAllProducts(@Context HttpServletRequest request) { String clientIp = request.getRemoteAddr(); // Verificar rate limit (implementação simplificada) AtomicInteger count = requestCounts.computeIfAbsent(clientIp, k -> new AtomicInteger(0)); if (count.incrementAndGet() > MAX_REQUESTS_PER_MINUTE) { return Response.status(429) // Too Many Requests .entity(new ErrorResponse("RATE_LIMIT_EXCEEDED", "Muitas requisições. Tente novamente em 60 segundos.")) .header("Retry-After", "60") .build(); } return Response.ok(products).build(); } }
🎯 Resumo do Capítulo
Parabéns! Você acabou de dominar a criação de APIs RESTful com Quarkus! 🎉
O que você conquistou:
✅ Entendeu os fundamentos de REST e JAX-RS
✅ Criou endpoints completos (CRUD)
✅ Implementou tratamento robusto de erros
✅ Adicionou validação profissional de dados
✅ Aprendeu sobre múltiplos formatos (JSON/XML)
✅ Descobriu dicas avançadas para APIs de produção
Sua API já está funcional e pronta para o mundo real! Mas ainda temos muito a explorar...
🔗 Continue a Jornada
👉 Capítulo 6: Persistência de Dados com Panache (Hibernate ORM) - Vamos conectar nossa API a um banco de dados utilizando Panache
💬 Vamos Conversar!
Gostou do capítulo? Tem alguma dúvida sobre APIs RESTful?
- 👍 Curta se foi útil
- 💬 Comente suas dúvidas ou experiências
- 🔔 Siga para não perder os próximos capítulos
- 🔄 Compartilhe com outros devs
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.