Aula 3: Jakarta REST (JAX-RS) e APIs RESTful
Objetivos
- Compreender os princípios arquiteturais REST e o protocolo HTTP
- Dominar as anotações JAX-RS para construção de endpoints (
@Path,@GET,@POST,@PUT,@DELETE) - Implementar serialização e desserialização JSON com Jackson
- Aplicar boas práticas de design de APIs RESTful (verbos, status codes, negociação de conteúdo)
- Construir uma API REST completa integrando Records, Streams e CDI com Quarkus 3.31
Contexto e Motivação
Nas aulas anteriores, criamos uma aplicação Quarkus e revisamos o Java moderno. Mas como expor funcionalidades para o mundo externo? Como permitir que um frontend, um aplicativo móvel ou outro sistema consuma os dados da nossa aplicação?
A resposta é REST (Representational State Transfer) — o estilo arquitetural dominante para comunicação entre sistemas na web moderna. REST não é um protocolo nem uma tecnologia específica, mas um conjunto de princípios que define como recursos devem ser expostos e manipulados através do HTTP.
No ecossistema Jakarta EE, a especificação Jakarta REST (anteriormente JAX-RS — Java API for RESTful Web Services) padroniza a construção de APIs RESTful em Java. Assim como JPA padroniza persistência e CDI padroniza injeção de dependências, Jakarta REST padroniza a camada web.
Jakarta REST é a especificação. RESTEasy Reactive é a implementação usada pelo Quarkus, otimizada para non-blocking I/O e build-time processing. O código que você escreve usa a API padrão Jakarta REST — funciona em qualquer runtime compatível.
Por que aprender JAX-RS formalmente?
Embora tenhamos usado anotações como @Path e @GET nos laboratórios anteriores, é fundamental compreender o modelo completo da especificação:
| Aspecto | Sem conhecimento formal | Com domínio de JAX-RS |
|---|---|---|
| Status codes | Sempre retorna 200 ou 500 | Retorna códigos semânticos corretos (201, 204, 404, 409) |
| Verbos HTTP | Tudo via GET | GET para leitura, POST para criação, PUT para atualização, DELETE para remoção |
| Negociação de conteúdo | Sempre JSON hardcoded | Suporta JSON, XML e outros formatos via Accept header |
| Tratamento de erros | Exceções genéricas | Respostas estruturadas com ExceptionMapper |
| Validação | Manual no código | Integrada com Bean Validation (@Valid) |
Conteúdo
Fundamentos REST e HTTP
REST foi definido por Roy Fielding em sua dissertação de doutorado (2000) como um estilo arquitetural baseado em seis restrições:
- Client-Server: Separação de responsabilidades entre cliente e servidor
- Stateless: Cada requisição contém toda informação necessária — o servidor não mantém estado da sessão
- Cacheable: Respostas devem indicar se podem ser cacheadas
- Uniform Interface: Interface uniforme baseada em recursos, representações e hipermídia
- Layered System: Arquitetura em camadas (proxies, gateways, load balancers)
- Code on Demand (opcional): Servidor pode enviar código executável ao cliente
Recursos e URIs
Em REST, tudo é um recurso identificado por uma URI (Uniform Resource Identifier):
/produtos → Coleção de produtos
/produtos/42 → Produto específico com ID 42
/produtos/42/avaliacoes → Avaliações do produto 42
/usuarios/joao/listas → Listas do usuário "joao"
Boas práticas para URIs:
- Usar substantivos no plural (
/produtos, não/getProdutos) - Usar hierarquia para representar pertencimento (
/usuarios/1/avaliacoes) - Evitar verbos na URI — o verbo HTTP é quem define a ação
- Minúsculo com hifens:
/itens-culturais, não/ItensCulturais
Verbos HTTP e Semântica
| Verbo | Ação | Idempotente | Corpo na Requisição | Exemplo |
|---|---|---|---|---|
| GET | Ler recurso(s) | Sim | Não | GET /produtos |
| POST | Criar recurso | Não | Sim | POST /produtos |
| PUT | Substituir recurso completo | Sim | Sim | PUT /produtos/42 |
| PATCH | Atualizar parcialmente | Não | Sim | PATCH /produtos/42 |
| DELETE | Remover recurso | Sim | Não | DELETE /produtos/42 |
Idempotente significa que executar a operação múltiplas vezes produz o mesmo resultado que executá-la uma vez. DELETE /produtos/42 executado duas vezes tem o mesmo efeito: o produto 42 não existe mais. Já POST /produtos executado duas vezes pode criar dois recursos distintos.
Códigos de Status HTTP
Os códigos de resposta HTTP comunicam o resultado da operação de forma padronizada:
| Faixa | Categoria | Códigos Comuns |
|---|---|---|
| 2xx | Sucesso | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirecionamento | 301 Moved Permanently, 304 Not Modified |
| 4xx | Erro do cliente | 400 Bad Request, 404 Not Found, 409 Conflict, 422 Unprocessable Entity |
| 5xx | Erro do servidor | 500 Internal Server Error, 503 Service Unavailable |
Quando usar cada código de sucesso:
200 OK— Operação bem-sucedida com corpo de resposta (GET, PUT)201 Created— Recurso criado com sucesso (POST) — inclua headerLocation204 No Content— Operação bem-sucedida sem corpo de resposta (DELETE)
Jakarta REST (JAX-RS): A Especificação
Jakarta REST define um conjunto de anotações e interfaces que mapeiam classes Java para recursos HTTP.
Anotações Fundamentais
@Path("/produtos") // Define a URI base do recurso
@Produces(MediaType.APPLICATION_JSON) // Formato padrão de resposta
@Consumes(MediaType.APPLICATION_JSON) // Formato aceito na requisição
public class ProdutoResource {
@GET // Mapeia para HTTP GET
public List<ProdutoDTO> listar() { ... }
@GET
@Path("/{id}") // Parâmetro de caminho
public ProdutoDTO buscarPorId(
@PathParam("id") Long id // Extrai valor da URI
) { ... }
@POST // Mapeia para HTTP POST
public Response criar(ProdutoDTO produto) { ... }
@PUT
@Path("/{id}") // Mapeia para HTTP PUT
public ProdutoDTO atualizar(
@PathParam("id") Long id,
ProdutoDTO produto
) { ... }
@DELETE
@Path("/{id}") // Mapeia para HTTP DELETE
public void remover(@PathParam("id") Long id) { ... }
}
| Anotação | Função | Exemplo |
|---|---|---|
@Path | Define o caminho do recurso | @Path("/produtos") |
@GET, @POST, @PUT, @DELETE | Mapeia o verbo HTTP | @GET no método de leitura |
@PathParam | Extrai parâmetro da URI | @PathParam("id") Long id |
@QueryParam | Extrai parâmetro da query string | @QueryParam("nome") String nome |
@HeaderParam | Extrai valor de um header HTTP | @HeaderParam("Authorization") String auth |
@Produces | Define o formato da resposta | @Produces(MediaType.APPLICATION_JSON) |
@Consumes | Define o formato aceito | @Consumes(MediaType.APPLICATION_JSON) |
Parâmetros de Requisição
@GET
@Path("/busca")
public List<ProdutoDTO> buscar(
@QueryParam("nome") String nome, // ?nome=notebook
@QueryParam("categoria") String categoria, // &categoria=eletronicos
@QueryParam("precoMin") BigDecimal precoMin, // &precoMin=100
@QueryParam("precoMax") BigDecimal precoMax, // &precoMax=5000
@QueryParam("pagina") @DefaultValue("0") int pagina, // &pagina=0
@QueryParam("tamanho") @DefaultValue("10") int tamanho // &tamanho=10
) {
return service.buscar(nome, categoria, precoMin, precoMax, pagina, tamanho);
}
Use @DefaultValue para definir valores padrão quando o parâmetro não é informado. Isso evita NullPointerException e torna a API mais amigável.
Construindo Respostas com Response
A classe Response permite controle total sobre a resposta HTTP:
@POST
public Response criar(ProdutoDTO produto) {
ProdutoDTO criado = service.criar(produto);
// Retorna 201 Created com header Location e corpo
return Response
.status(Response.Status.CREATED) // 201
.header("Location", "/produtos/" + criado.id()) // URI do recurso criado
.entity(criado) // Corpo da resposta
.build();
}
@DELETE
@Path("/{id}")
public Response remover(@PathParam("id") Long id) {
boolean removido = service.remover(id);
if (removido) {
return Response.noContent().build(); // 204 No Content
}
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("erro", "Produto não encontrado"))
.build(); // 404 Not Found
}
Serialização JSON com Jackson
O Quarkus usa Jackson para converter objetos Java em JSON e vice-versa. Records Java são automaticamente serializáveis:
// Este record...
public record ProdutoDTO(Long id, String nome, BigDecimal preco, boolean ativo) {}
// ...é serializado automaticamente para:
// { "id": 1, "nome": "Notebook", "preco": 2500.00, "ativo": true }
Customização com anotações Jackson:
public record ProdutoDTO(
Long id,
String nome,
BigDecimal preco,
boolean ativo,
@JsonProperty("categoria_nome") // Nome customizado no JSON
String categoriaNome,
@JsonIgnore // Excluído do JSON
String dadoInterno,
@JsonFormat(pattern = "dd/MM/yyyy") // Formato de data
LocalDate dataCriacao
) {}
Tratamento de Erros com ExceptionMapper
Em vez de tratar exceções em cada endpoint, Jakarta REST permite centralizar o tratamento:
// Exceção de domínio
public class RecursoNaoEncontradoException extends RuntimeException {
public RecursoNaoEncontradoException(String mensagem) {
super(mensagem);
}
}
// Mapper que converte a exceção em resposta HTTP
@Provider // Registra automaticamente no container
public class RecursoNaoEncontradoMapper
implements ExceptionMapper<RecursoNaoEncontradoException> {
@Override
public Response toResponse(RecursoNaoEncontradoException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of(
"erro", e.getMessage(),
"tipo", "RECURSO_NAO_ENCONTRADO",
"timestamp", LocalDateTime.now().toString()
))
.type(MediaType.APPLICATION_JSON)
.build();
}
}
Agora, qualquer endpoint que lançar RecursoNaoEncontradoException retornará automaticamente um 404 estruturado:
@GET
@Path("/{id}")
public ProdutoDTO buscarPorId(@PathParam("id") Long id) {
return service.buscarPorId(id)
.orElseThrow(() -> new RecursoNaoEncontradoException(
"Produto não encontrado com ID: " + id));
}
Integração com CDI
Jakarta REST integra-se naturalmente com CDI para injeção de dependências:
@Path("/produtos")
@ApplicationScoped // Bean CDI
@Produces(MediaType.APPLICATION_JSON)
public class ProdutoResource {
@Inject // CDI injeta o serviço
ProdutoService service;
@GET
public List<ProdutoDTO> listar() {
return service.buscarProdutosAtivos(); // Delega para a camada de serviço
}
}
Esta separação em camadas (Resource → Service → Repository) é um padrão arquitetural fundamental — o Resource cuida apenas de HTTP, enquanto o Service concentra a lógica de negócio.
Laboratório 2 — Construindo uma API REST Completa
Objetivo
Construir uma API REST funcional com múltiplos endpoints, integrando Records, Streams API, CDI e boas práticas de design RESTful com Quarkus 3.31.
Pré-requisitos
- Projeto Quarkus criado na Aula 1 (Laboratório 1)
- JDK 21 ou superior instalado
- Quarkus CLI ou Maven/Gradle configurado
Passo 1: Definindo o Modelo de Dados com Records
Crie o pacote dto em src/main/java/br/upf/ads175:
// CategoriaDTO.java
package br.upf.ads175.dto;
public record CategoriaDTO(String nome) {}
// ProdutoDTO.java
package br.upf.ads175.dto;
import java.math.BigDecimal;
public record ProdutoDTO(
Long id,
String nome,
BigDecimal preco,
boolean ativo,
CategoriaDTO categoria
) {
public boolean isPremium() {
return preco.compareTo(BigDecimal.valueOf(1000)) > 0;
}
}
Passo 2: Criando a Camada de Serviço com CDI
Crie o pacote service e a classe ProdutoService.java:
package br.upf.ads175.service;
import br.upf.ads175.dto.CategoriaDTO;
import br.upf.ads175.dto.ProdutoDTO;
import jakarta.enterprise.context.ApplicationScoped;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
@ApplicationScoped
public class ProdutoService {
private List<ProdutoDTO> obterProdutosMock() {
return List.of(
new ProdutoDTO(1L, "Notebook Gamer", new BigDecimal("8500.00"), true, new CategoriaDTO("Eletrônicos")),
new ProdutoDTO(2L, "Cadeira de Escritório", new BigDecimal("1200.50"), true, new CategoriaDTO("Móveis")),
new ProdutoDTO(3L, "Monitor 4K", new BigDecimal("2300.00"), false, new CategoriaDTO("Eletrônicos")),
new ProdutoDTO(4L, "Mesa de Escritório", new BigDecimal("850.00"), true, new CategoriaDTO("Móveis")),
new ProdutoDTO(5L, "Teclado Mecânico", new BigDecimal("450.00"), true, new CategoriaDTO("Eletrônicos")),
new ProdutoDTO(6L, "Smartphone Premium", new BigDecimal("3500.00"), true, new CategoriaDTO("Eletrônicos"))
);
}
public List<ProdutoDTO> buscarProdutosAtivosOrdenadosPorNome() {
return obterProdutosMock().stream()
.filter(ProdutoDTO::ativo)
.sorted(Comparator.comparing(ProdutoDTO::nome))
.toList();
}
public Optional<ProdutoDTO> buscarPorId(Long id) {
return obterProdutosMock().stream()
.filter(produto -> produto.id().equals(id))
.findFirst();
}
public Map<String, List<String>> buscarNomesProdutosAgrupadosPorCategoria() {
return obterProdutosMock().stream()
.collect(Collectors.groupingBy(
produto -> produto.categoria().nome(),
Collectors.mapping(ProdutoDTO::nome, Collectors.toList())
));
}
public List<ProdutoDTO> buscarProdutosPremium() {
return obterProdutosMock().stream()
.filter(ProdutoDTO::ativo)
.filter(ProdutoDTO::isPremium)
.sorted(Comparator.comparing(ProdutoDTO::preco).reversed())
.toList();
}
public Map<String, Map<String, Object>> obterEstatisticasPorCategoria() {
return obterProdutosMock().stream()
.filter(ProdutoDTO::ativo)
.collect(Collectors.groupingBy(
produto -> produto.categoria().nome(),
Collectors.collectingAndThen(
Collectors.toList(),
produtos -> Map.of(
"quantidade", produtos.size(),
"precoMedio", produtos.stream()
.map(ProdutoDTO::preco)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.divide(BigDecimal.valueOf(produtos.size())),
"precoMaximo", produtos.stream()
.map(ProdutoDTO::preco)
.max(BigDecimal::compareTo)
.orElse(BigDecimal.ZERO)
)
)
));
}
}
Passo 3: Expondo a Lógica via API REST
Crie o pacote resource e a classe ProdutoResource.java:
package br.upf.ads175.resource;
import br.upf.ads175.dto.ProdutoDTO;
import br.upf.ads175.service.ProdutoService;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.Map;
@Path("/produtos")
@Produces(MediaType.APPLICATION_JSON)
public class ProdutoResource {
@Inject
ProdutoService service;
@GET
public List<ProdutoDTO> obterProdutosAtivos() {
return service.buscarProdutosAtivosOrdenadosPorNome();
}
@GET
@Path("/por-categoria")
public Map<String, List<String>> obterProdutosAgrupados() {
return service.buscarNomesProdutosAgrupadosPorCategoria();
}
@GET
@Path("/{id}")
public Response obterProdutoPorId(@PathParam("id") Long id) {
return service.buscarPorId(id)
.map(produto -> Response.ok(produto).build())
.orElse(Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("erro", "Produto não encontrado", "id", id))
.build());
}
@GET
@Path("/premium")
public List<ProdutoDTO> obterProdutosPremium() {
return service.buscarProdutosPremium();
}
@GET
@Path("/estatisticas")
public Map<String, Map<String, Object>> obterEstatisticas() {
return service.obterEstatisticasPorCategoria();
}
}
Passo 4: Testando a Aplicação
Execute com live coding:
quarkus dev
Teste os endpoints:
- Browser
- cURL
- http://localhost:8080/produtos — Lista de produtos ativos
- http://localhost:8080/produtos/por-categoria — Agrupamento por categoria
- http://localhost:8080/produtos/1 — Produto específico
- http://localhost:8080/produtos/premium — Produtos premium
- http://localhost:8080/produtos/estatisticas — Estatísticas
curl -s http://localhost:8080/produtos | jq
curl -s http://localhost:8080/produtos/por-categoria | jq
curl -s http://localhost:8080/produtos/1 | jq
curl -s http://localhost:8080/produtos/999 | jq # Teste o 404
curl -s http://localhost:8080/produtos/premium | jq
curl -s http://localhost:8080/produtos/estatisticas | jq
Passo 5: Adicionando um ExceptionMapper
Crie src/main/java/br/upf/ads175/exception/ErroResponse.java:
package br.upf.ads175.exception;
import java.time.LocalDateTime;
public record ErroResponse(
String erro,
String tipo,
int status,
LocalDateTime timestamp
) {
public ErroResponse(String erro, String tipo, int status) {
this(erro, tipo, status, LocalDateTime.now());
}
}
Crie src/main/java/br/upf/ads175/exception/GenericExceptionMapper.java:
package br.upf.ads175.exception;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
@Provider
public class GenericExceptionMapper implements ExceptionMapper<IllegalArgumentException> {
@Override
public Response toResponse(IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErroResponse(e.getMessage(), "VALIDACAO", 400))
.type(MediaType.APPLICATION_JSON)
.build();
}
}
Exercícios (Checkpoints)
- Explique a diferença entre
@PathParame@QueryParame dê exemplos de quando usar cada um. - Implemente um endpoint
GET /produtos/baratosque retorne produtos com preço inferior a R$ 500,00 usando@QueryParam("precoMaximo")com valor padrão. - Crie um endpoint
POST /produtosque receba umProdutoDTOno corpo da requisição e retorne201 Createdcom headerLocation. - Implemente um
ExceptionMapperparaNullPointerExceptionque retorne500 Internal Server Errorcom mensagem amigável. - Descreva por que
DELETEé idempotente masPOSTnão é. Dê exemplos práticos.