DEV Community

Cover image for Capítulo 5: Construindo APIs RESTful com RESTEasy
Eduardo R. Ferreira
Eduardo R. Ferreira

Posted on

Capítulo 5: Construindo APIs RESTful com RESTEasy

📚 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);
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

🤔 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

🎯 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
Enter fullscreen mode Exit fullscreen mode

Agora você pode testar os endpoints:

📋 Listar todos os produtos

curl http://localhost:8080/products
Enter fullscreen mode Exit fullscreen mode

🔍 Buscar produto específico

curl http://localhost:8080/products/1
Enter fullscreen mode Exit fullscreen mode

➕ 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
  }'
Enter fullscreen mode Exit fullscreen mode

✏️ 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
  }'
Enter fullscreen mode Exit fullscreen mode

🗑️ Deletar produto

curl -X DELETE http://localhost:8080/products/1
Enter fullscreen mode Exit fullscreen mode

⚠️ 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");
    }
}
Enter fullscreen mode Exit fullscreen mode
package com.example.exception;

public class InvalidProductDataException extends RuntimeException {
    public InvalidProductDataException(String message) {
        super(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode
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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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
  }'
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Testando XML

Receber resposta em XML:

curl -H "Accept: application/xml" http://localhost:8080/products/1
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>'
Enter fullscreen mode Exit fullscreen mode

🎭 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"})
Enter fullscreen mode Exit fullscreen mode

Para texto simples:

@GET
@Path("/info")
@Produces(MediaType.TEXT_PLAIN)
public String getInfo() {
    return "API de Produtos v1.0 - Total de produtos: " + products.size();
}
Enter fullscreen mode Exit fullscreen mode

🚀 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();
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

4. Documentação com OpenAPI/Swagger

Adicione documentação automática:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

Acesse a documentação:

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
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

🎯 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.