Este artigo faz parte da série “Quarkus for Spring Developers”.

No mundo das aplicações modernas, a comunicação entre sistemas é predominantemente realizada através de APIs RESTful. Se você vem do universo Spring, está acostumado a construir esses endpoints com o Spring MVC ou, mais recentemente, com o Spring WebFlux. No Quarkus, o caminho é igualmente direto, mas com nuances e otimizações que o tornam uma escolha poderosa para a era Cloud Native.

Neste artigo, vamos mergulhar na construção de APIs RESTful com Quarkus, explorando o padrão Jakarta REST (anteriormente JAX-RS) e sua implementação de alta performance, o Quarkus REST (anteriormente chamado de RESTEasy Reactive). Vamos comparar as abordagens com o Spring, focando em como mapear URLs, negociar conteúdo, gerenciar códigos de status e lidar com exceções de forma eficiente, além de cobrir a importância dos testes.

Mãos à obra: Adicionando Suporte a JSON

Se você seguiu o artigo anterior, seu projeto já conta com a extensão quarkus-rest. Agora, para que possamos enviar e receber dados no formato JSON, precisamos adicionar a extensão que integra o motor REST com o Jackson.

Via Quarkus CLI:

quarkus ext add io.quarkus:quarkus-rest-jackson rest-assured

Esta extensão é fundamental: ela fornece os componentes necessários para que o Quarkus saiba como transformar seus objetos Java (como a nossa classe Order) em JSON e vice-versa automaticamente.

Dica de Performance: Para executáveis nativos, habilite a otimização de serialização sem reflexão no application.properties: quarkus.rest.jackson.optimization.enable-reflection-free-serializers=true


Spring MVC vs. Jakarta REST (Quarkus REST)

Ambos os frameworks oferecem suporte robusto para construir aplicações RESTful. Conceitualmente, a abordagem é similar: você cria classes com métodos que representam recursos, declarando parâmetros de entrada e saída.

Jakarta REST (JAX-RS) e Quarkus REST

O Quarkus utiliza o Jakarta REST para definir seus endpoints. Diferente do JAX-RS tradicional rodando em containers de servlet, no Quarkus:

  • Sem Classe Application: Não há necessidade de definir uma classe Application. O Quarkus a cria automaticamente.
  • CDI por Padrão: Todos os recursos são tratados como beans CDI escopados como Singleton por padrão.

Runtime Subjacente: Vert.x e Flexibilidade Reativa

Uma diferença fundamental reside na escolha do runtime. Enquanto o Spring MVC historicamente se baseou na Especificação Servlet síncrona, o Quarkus utiliza o motor reativo de event-loop do Eclipse Vert.x como padrão.

O Quarkus REST processa requisições diretamente na thread de I/O para throughput máximo (similar ao Spring WebFlux). Para métodos que realizam trabalho bloqueante, basta usar a anotação @Blocking. Isso sinaliza ao Quarkus para mover a execução para um pool de threads de worker, liberando a thread de I/O para outras requisições.

Fato de Performance: De acordo com Georgios Andrianakis em “Massive performance without headaches”, um endpoint Quarkus REST usando @Blocking ainda atinge um throughput 50% superior ao modelo RESTEasy Classic (baseado em servlets), devido à integração profunda com o Vert.x e otimizações de tempo de build.

Bibliotecas Reativas: Mutiny vs. Project Reactor

Desenvolvedores Spring WebFlux conhecem o Mono e o Flux. No Quarkus, usamos o SmallRye Mutiny, focado em eventos:

  • Uni: Equivale ao Mono (um único resultado assíncrono ou falha).
  • Multi: Equivale ao Flux (fluxo de múltiplos itens com suporte a backpressure).

Anatomia dos Endpoints: Estrutura e Parâmetros

A construção de endpoints no Quarkus segue o padrão Jakarta REST, separando as responsabilidades de roteamento, método e conteúdo.

Como mapear URLs e caminhos?

FuncionalidadeQuarkus (Jakarta REST)Spring EquivalentLocalizaçãoDescrição
Caminho Base@Path("/orders")@RequestMappingClassePrefixo para todos os caminhos da classe.
Sub-caminho@Path("/{id}")Atributo path de @GetMappingMétodoCaminho específico do endpoint.
Verbo GET@GET@GetMappingMétodoMapeia requisições HTTP GET.
Verbo POST@POST@PostMappingMétodoMapeia requisições HTTP POST.
Produz@ProducesAtributo producesClasse/MétodoDefine o formato de saída (Default: JSON).
Consome@ConsumesAtributo consumesClasse/MétodoDefine o formato de entrada (Default: JSON).

Injeção de Parâmetros

O Quarkus REST introduziu anotações concisas que facilitam a leitura:

DescriçãoQuarkus (Jakarta REST)Spring Equivalent
Variável no Path@RestPath@PathVariable
Parâmetro de Query@RestQuery@RequestParam
Valor de Header@RestHeader@RequestHeader
Valor de Cookie@RestCookie@CookieValue
Corpo da RequisiçãoN/A (Injeção direta)@RequestBody

Valores de Retorno Comuns

DescriçãoQuarkus (Jakarta REST)Spring Equivalent
Resposta CompletaResponseResponseEntity<>
Sem Corpovoidvoid (Retorna 204)
Valor Único ReativoUni<T>Mono<T>
Stream ReativoMulti<T>Flux<T>
Objeto POJOTT (Marshalled para JSON)

Comparativo de Código: Spring vs. Quarkus

Para quem vem do Spring, a melhor forma de entender o Quarkus é vendo os mesmos comportamentos implementados em ambas as tecnologias.

1. Estrutura da Classe Resource/Controller

// SPRING MVC
@RestController
@RequestMapping("/orders")
public class OrderController { }

// QUARKUS (Jakarta REST)
@Path("/orders")
public class OrderResource { }

2. Listagem de Recursos

// SPRING MVC
@GetMapping
public List<Order> list() {
    return service.getOrders();
}

// QUARKUS
@GET
public List<Order> list() {
    return service.getOrders();
}

3. Busca por ID com Status 404 (Not Found)

// SPRING MVC
@GetMapping("/{id}")
public ResponseEntity<Order> get(@PathVariable Long id) {
    return service.findById(id)
            .map(ResponseEntity::ok)
            .orElseGet(() -> ResponseEntity.notFound().build());
}

// QUARKUS
@GET
@Path("/{id}")
public Response get(@RestPath Long id) {
    return service.findById(id)
            .map(order -> Response.ok(order).build())
            .orElseGet(() -> Response.status(Status.NOT_FOUND).build());
}

4. Criação de Recursos (201 Created)

// SPRING MVC
@PostMapping
public ResponseEntity<Order> create(@Valid @RequestBody Order order) {
    Order saved = service.save(order);
    return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}

// QUARKUS
@POST
public Response create(@Valid Order order) {
    Order saved = service.save(order);
    return Response.status(Response.Status.CREATED).entity(saved).build();
}

5. Server-Sent Events (SSE)

// SPRING WEBFLUX
@GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Order> stream() {
    return service.getOrderStream();
}

// QUARKUS
@GET
@Path("/stream")
@Produces(MediaType.SERVER_SENT_EVENTS)
public Multi<Order> stream() {
    return service.getOrderStream();
}

6. Tratamento de Exceções

// SPRING (@RestControllerAdvice)
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorMessage> handle(RuntimeException ex) {
    return ResponseEntity.status(500).body(new ErrorMessage(ex.getMessage()));
}

// QUARKUS (@ServerExceptionMapper)
@ServerExceptionMapper(RuntimeException.class)
public Response mapException(RuntimeException ex) {
    return Response.serverError()
            .header("X-ERROR-TYPE", "BUSINESS_FAILURE")
            .entity(new ErrorMessage(ex.getMessage(), 500))
            .build();
}

Funcionalidades Avançadas e “Exclusivas”

1. Controle de Cache e Segurança de Campos

Use @Cache para gerenciar o header Cache-Control e @SecureField no seu POJO para omitir campos sensíveis (como profitMargin) baseados nas roles do usuário logado.

public class Order {
    public Long id;
    public String customerName;
    @SecureField(rolesAllowed = "ADMIN")
    public double profitMargin;
}

2. Multipart e Upload de Arquivos

O acesso a arquivos é nativo através da classe FileUpload e da anotação @RestForm.


A Grande Síntese: O Sistema de Pedidos Completo

Vamos consolidar tudo em uma implementação profissional, expandindo o que começamos no post de Injeção de Dependência.

Passo 1: Expansão do OrderService

Simularemos a persistência em memória com logs e lógica de erro.

@ApplicationScoped
public class OrderService {

    private static final Logger LOG = Logger.getLogger(OrderService.class);
    private List<Order> orders = new ArrayList<>();

    public Order save(Order order) {
        order.id = (long) (orders.size() + 1);
        orders.add(order);
        LOG.infof("Order saved with ID: %d", order.id);
        return order;
    }

    public List<Order> getOrders() {
        LOG.info("Fetching all orders from memory");
        return orders;
    }

    public Optional<Order> findById(Long id) {
        LOG.infof("Finding order: %d", id);
        return orders.stream().filter(o -> o.id.equals(id)).findFirst();
    }

    public void delete(Long id) {
        LOG.infof("Removing order: %d", id);
        orders.removeIf(o -> o.id.equals(id));
    }

    public void performWorkGeneratingError() {
        LOG.error("Simulating a business error...");
        throw new RuntimeException("Unexpected order processing failure");
    }

    public Multi<Order> getOrderStream() {
        return Multi.createFrom().ticks().every(Duration.ofSeconds(1))
                .onItem().transform(tick -> orders.isEmpty() ? null : orders.get(tick.intValue() % orders.size()))
                .select().where(Objects::nonNull)
                .select().first(orders.size());
    }
}

Passo 2: O OrderResource Definitivo

Esta versão definitiva utiliza logs, Exception Mapping e SSE.

@Path("/orders")
public class OrderResource {

    private static final Logger LOG = Logger.getLogger(OrderResource.class);

    @Inject
    OrderService service;

    @POST
    public Response create(Order order) {
        LOG.info("Creating new order for customer: " + order.customerName);
        Order created = service.save(order);
        return Response.status(Response.Status.CREATED).entity(created).build();
    }

    @GET
    public List<Order> list() {
        return service.getOrders();
    }

    @GET
    @Path("/{id}")
    public Response get(@RestPath Long id) {
        return service.findById(id)
                .map(p -> Response.ok(p).build())
                .orElseGet(() -> Response.status(Status.NOT_FOUND).build());
    }

    @DELETE
    @Path("/{id}")
    public Response delete(@RestPath Long id) {
        service.delete(id);
        return Response.noContent().build();
    }

    @GET
    @Path("/stream")
    @Produces(MediaType.SERVER_SENT_EVENTS)
    public Multi<Order> streamOrders() {
        return service.getOrderStream();
    }

    @GET
    @Path("/error")
    public void generateError() {
        service.performWorkGeneratingError();
    }

    @ServerExceptionMapper(RuntimeException.class)
    public Response mapException(RuntimeException ex) {
        LOG.error("Mapping exception to user-friendly response");
        return Response.serverError()
                .header("X-ERROR-TYPE", "BUSINESS_FAILURE")
                .entity(new ErrorMessage(ex.getMessage(), 500))
                .build();
    }
}

Testes de Integração com REST-assured

No Quarkus, injetamos a Service real para preparar o estado do sistema e validamos o fluxo HTTP completo.

@QuarkusTest
class OrderResourceTest {

    @Inject
    OrderService service;

    @Test
    void testCreateOrder() {
        Order order = new Order(null, "Matheus", 150.0);

        given()
            .contentType(ContentType.JSON)
            .body(order)
          .when().post("/orders")
          .then()
             .statusCode(201)
             .body("id", notNullValue(),
                   "customerName", is("Matheus"),
                   "totalAmount", is(150.0f));
    }

    @Test
    void testListOrders() {
        // Garantindo que existe ao menos um dado
        service.save(new Order(null, "Test User", 100.0));

        given()
          .when().get("/orders")
          .then()
             .statusCode(200)
             .body("$.size()", greaterThan(0));
    }

    @Test
    void testGetOrderById() {
        Order saved = service.save(new Order(null, "Detail Test", 200.0));

        given()
          .when().get("/orders/" + saved.id)
          .then()
             .statusCode(200)
             .body("id", is(saved.id.intValue()))
             .body("customerName", is("Detail Test"));
    }

    @Test
    void testGetOrderNotFound() {
        given()
          .when().get("/orders/999")
          .then()
             .statusCode(404);
    }

    @Test
    void testDeleteOrder() {
        Order saved = service.save(new Order(null, "To Delete", 50.0));

        given()
          .when().delete("/orders/" + saved.id)
          .then()
             .statusCode(204);
    }

    @Test
    void testGenerateError() {
        given()
          .when().get("/orders/error")
          .then()
             .statusCode(500)
             .header("X-ERROR-TYPE", is("BUSINESS_FAILURE"))
             .body("message", is("Unexpected order processing failure"));
    }
}

Conclusão

Construir APIs RESTful com Quarkus e Jakarta REST oferece uma experiência de desenvolvimento otimizada para a era Cloud Native. A unificação dos modelos síncronos e assíncronos em um único runtime elimina barreiras e escala sua aplicação de forma eficiente sem a necessidade de aprender frameworks drasticamente diferentes para casos reativos.

No próximo artigo, mergulharemos no Bean Validation e DTOs, garantindo que as nossas entradas de dados sejam robustas e bem estruturadas!


Recursos