Pular para o conteúdo principal

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.

observação

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:

AspectoSem conhecimento formalCom domínio de JAX-RS
Status codesSempre retorna 200 ou 500Retorna códigos semânticos corretos (201, 204, 404, 409)
Verbos HTTPTudo via GETGET para leitura, POST para criação, PUT para atualização, DELETE para remoção
Negociação de conteúdoSempre JSON hardcodedSuporta JSON, XML e outros formatos via Accept header
Tratamento de errosExceções genéricasRespostas estruturadas com ExceptionMapper
ValidaçãoManual no códigoIntegrada 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:

  1. Client-Server: Separação de responsabilidades entre cliente e servidor
  2. Stateless: Cada requisição contém toda informação necessária — o servidor não mantém estado da sessão
  3. Cacheable: Respostas devem indicar se podem ser cacheadas
  4. Uniform Interface: Interface uniforme baseada em recursos, representações e hipermídia
  5. Layered System: Arquitetura em camadas (proxies, gateways, load balancers)
  6. 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

VerboAçãoIdempotenteCorpo na RequisiçãoExemplo
GETLer recurso(s)SimNãoGET /produtos
POSTCriar recursoNãoSimPOST /produtos
PUTSubstituir recurso completoSimSimPUT /produtos/42
PATCHAtualizar parcialmenteNãoSimPATCH /produtos/42
DELETERemover recursoSimNãoDELETE /produtos/42
dica

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:

FaixaCategoriaCódigos Comuns
2xxSucesso200 OK, 201 Created, 204 No Content
3xxRedirecionamento301 Moved Permanently, 304 Not Modified
4xxErro do cliente400 Bad Request, 404 Not Found, 409 Conflict, 422 Unprocessable Entity
5xxErro do servidor500 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 header Location
  • 204 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çãoFunçãoExemplo
@PathDefine o caminho do recurso@Path("/produtos")
@GET, @POST, @PUT, @DELETEMapeia o verbo HTTP@GET no método de leitura
@PathParamExtrai parâmetro da URI@PathParam("id") Long id
@QueryParamExtrai parâmetro da query string@QueryParam("nome") String nome
@HeaderParamExtrai valor de um header HTTP@HeaderParam("Authorization") String auth
@ProducesDefine o formato da resposta@Produces(MediaType.APPLICATION_JSON)
@ConsumesDefine 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);
}
dica

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:

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)

  1. Explique a diferença entre @PathParam e @QueryParam e dê exemplos de quando usar cada um.
  2. Implemente um endpoint GET /produtos/baratos que retorne produtos com preço inferior a R$ 500,00 usando @QueryParam("precoMaximo") com valor padrão.
  3. Crie um endpoint POST /produtos que receba um ProdutoDTO no corpo da requisição e retorne 201 Created com header Location.
  4. Implemente um ExceptionMapper para NullPointerException que retorne 500 Internal Server Error com mensagem amigável.
  5. Descreva por que DELETE é idempotente mas POST não é. Dê exemplos práticos.

Referências