Java EE / Jakarta EE

Jakarta REST (JAX-RS 3.1): como criar e consumir APIs HTTP

Intermediário9 min de leituraAtualizado

Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês

Jakarta RESTful Web Services (JAX-RS) é a API da plataforma, orientada a anotações, para serviços HTTP, e o modelo que Jersey, RESTEasy e Apache CXF implementam. Este guia mostra como as requisições são roteadas para os métodos de recurso, como parâmetros e corpos JSON são vinculados, como retornar os códigos de status e corpos de erro certos, como filtros, interceptors e exception mappers adicionam comportamento transversal e como chamar outros serviços com a API cliente — além dos erros de design e de segurança que aparecem em APIs reais.

O que você vai aprender

  • Declarar uma aplicação e classes de recurso e entender como as requisições são associadas a elas
  • Vincular parâmetros de path, de query, de header e de corpo e retornar objetos Response com os códigos de status corretos
  • Mapear exceções para respostas de erro e aplicar Bean Validation às entradas
  • Adicionar filtros, interceptors e providers para autenticação, logging e tratamento de conteúdo
  • Consumir serviços com a API cliente do Jakarta REST e usar respostas assíncronas e Server-Sent Events

Jakarta RESTful Web Services — JAX-RS em qualquer conversa, e no nome do pacote até a mudança para jakarta.ws.rs — mapeia HTTP para métodos Java comuns. Você anota uma classe com um path e seus métodos com verbos HTTP, declara de onde vem cada parâmetro e retorna um objeto; o runtime cuida do roteamento, da negociação de conteúdo, da serialização e das respostas de erro. Enquanto a API de servlets expõe o HTTP como ele é, o Jakarta REST o expõe do jeito que um designer de APIs pensa: recursos, representações, códigos de status e media types. Os princípios de design de uma boa API HTTP pertencem à trilha de arquitetura; este guia trata de aplicá-los bem com esta API.

Aplicação, recursos e matching

Uma classe de recurso é uma classe comum, geralmente um bean CDI, com um @Path. Seus métodos levam uma anotação de método HTTP, opcionalmente um subpath e a declaração do que consomem e produzem. Uma única subclasse de Application com @ApplicationPath define a raiz da API e, se você a deixar vazia, registra todos os recursos e providers que o runtime encontrar.

package shop.api;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("/api")            // all resources live under /<context>/api
public class ShopApplication extends Application { }
package shop.api;

import java.net.URI;
import java.util.List;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;

@Path("/orders")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@RequestScoped
public class OrderResource {

    @Inject OrderService orders;
    @Context UriInfo uriInfo;           // request-scoped injection of URI details

    @GET
    public List<OrderSummary> list(@QueryParam("status") OrderStatus status,
                                   @QueryParam("page") @DefaultValue("0") int page,
                                   @QueryParam("size") @DefaultValue("20") int size) {
        return orders.list(status, page, size);
    }

    @GET
    @Path("/{id}")
    public OrderDetail get(@PathParam("id") long id) {
        return orders.find(id).orElseThrow(NotFoundException::new);   // 404
    }

    @POST
    public Response create(@Valid NewOrder request) {
        OrderDetail created = orders.place(request);
        URI location = uriInfo.getAbsolutePathBuilder().path(Long.toString(created.id())).build();
        return Response.created(location).entity(created).build();    // 201 + Location
    }

    @PATCH
    @Path("/{id}/status")
    public OrderDetail changeStatus(@PathParam("id") long id, StatusChange change) {
        return orders.changeStatus(id, change.status());
    }

    @DELETE
    @Path("/{id}")
    public void cancel(@PathParam("id") long id) {
        orders.cancel(id);                                             // void -> 204 No Content
    }
}

O matching acontece em etapas: o runtime escolhe a classe de recurso cujo @Path casa com o prefixo mais longo, depois um método cujo subpath e verbo HTTP casem e, por fim, filtra comparando o Content-Type da requisição com @Consumes e o header Accept com @Produces. Um path que casa sem nenhum verbo correspondente gera 405 Method Not Allowed; um verbo que casa sem media type compatível gera 415 Unsupported Media Type ou 406 Not Acceptable. Templates de path aceitam expressões regulares — @Path("/{id: \\d+}") — para que /orders/abc seja um 404 e não um erro de conversão. Um método com @Path mas sem anotação de verbo é um sub-resource locator: ele retorna outro objeto de recurso que continua o matching, e é assim que você extrai /orders/{id}/lines para uma classe própria.

Classes de recurso que não são beans CDI têm escopo de requisição por padrão; declarar @RequestScoped ou @ApplicationScoped explicitamente deixa o ciclo de vida óbvio e permite que o CDI injete dependências nelas. Um recurso com escopo de aplicação precisa ser thread-safe, e os campos @Context dele são proxies resolvidos a cada requisição.

Parâmetros e corpos

Os parâmetros vêm de várias origens, cada uma com sua anotação: @PathParam, @QueryParam, @HeaderParam, @CookieParam, @FormParam (corpos de formulário URL-encoded) e @MatrixParam. A conversão a partir da string trata tipos primitivos, enums, qualquer tipo com construtor String ou método estático valueOf/fromString e List<T> para valores repetidos; @DefaultValue cobre parâmetros ausentes. Um grupo de parâmetros relacionados pode ser reunido em uma classe com os campos anotados e injetado com @BeanParam, o que mantém curtas as assinaturas dos endpoints de busca.

O único parâmetro sem anotação é o corpo da entidade. Ele é lido por um MessageBodyReader escolhido pelo Content-Type: corpos JSON viram objetos via JSON Binding, String, byte[], InputStream e File são suportados nativamente, e o Jakarta REST 3.1 finalmente padronizou o multipart com EntityPart:

@POST
@Path("/{id}/attachments")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response attach(@PathParam("id") long id, List<EntityPart> parts) throws IOException {
    for (EntityPart part : parts) {
        if (part.getFileName().isPresent()) {
            orders.attach(id, part.getFileName().get(), part.getMediaType(), part.getContent());
        }
    }
    return Response.noContent().build();
}

Para classes tradicionais, o JSON Binding exige um construtor público sem argumentos e getters/setters ou campos públicos; records são suportados pelo Yasson 3, a implementação compatível de JSON-B, desde o JSON-B 3.0. Nomes de campos, formatos de data, tratamento de null e ordem das propriedades são controlados com @JsonbProperty, @JsonbDateFormat, @JsonbNillable e @JsonbPropertyOrder, ou globalmente com um JsonbConfig registrado por meio de um ContextResolver<Jsonb>. Mantenha os tipos da API (NewOrder, OrderDetail) separados das entidades JPA: serializar uma entidade percorre associações lazy, vaza campos internos e acopla sua API ao seu schema.

Respostas e códigos de status

Um método que retorna um objeto produz 200 OK com a entidade serializada, void produz 204 No Content e null também produz 204. Todo o resto passa por Response:

SituaçãoConstrua comStatus
Recurso criadoResponse.created(uri).entity(body)201 com Location
Aceito para processamento em segundo planoResponse.accepted()202
Nada a retornarResponse.noContent()204
GET condicional atendidorequest.evaluatePreconditions(etag) retorna um valor não nulo304
Erro do cliente com corpoResponse.status(Status.CONFLICT).entity(problem)409 etc.

Headers são adicionados com .header(...), cache com .cacheControl(...) e .tag(...), e Response.temporaryRedirect(uri) cobre redirecionamentos. Response também pode carregar uma GenericEntity para retornar uma List<T> preservando a informação de tipo quando o runtime não consegue inferi-la. Códigos de status têm significado para clientes e intermediários; a trilha de web traz uma referência de códigos de status para você conferir a semântica exata antes de escolher um.

Erros: exceções e mappers

Lançar uma subclasse de WebApplicationException — NotFoundException, BadRequestException, ForbiddenException, NotAuthorizedException, ClientErrorException(Response) — envia o status correspondente. Para ter um corpo de erro consistente em toda a API, registre um ExceptionMapper por tipo de exceção:

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

@Provider
public class DomainExceptionMapper implements ExceptionMapper<DomainException> {

    @Override
    public Response toResponse(DomainException e) {
        var problem = new Problem("https://api.example.com/problems/" + e.code(),
                                  e.getMessage(), e.status());
        return Response.status(e.status())
                       .type("application/problem+json")     // RFC 9457 problem details
                       .entity(problem)
                       .build();
    }
}

Classes @Provider são descobertas automaticamente. O runtime escolhe o mapper da superclasse mais próxima da exceção lançada, então um mapper para Throwable captura tudo o que os outros deixam passar — e deve registrar a stack trace no log e retornar um 500 genérico sem a mensagem da exceção, que pode conter SQL ou paths internos. Falhas de Bean Validation (ConstraintViolationException) são convertidas pelo runtime em 400 quando se referem a dados da requisição; adicione seu próprio mapper se quiser listar as violações no corpo. Nunca deixe uma PersistenceException chegar ao cliente sem mapeamento.

Validação

Anote os campos dos DTOs com restrições de Bean Validation e o parâmetro com @Valid, como em create acima; restrições diretamente nos parâmetros (@QueryParam("size") @Max(100) int size) também funcionam. A validação acontece antes de o método rodar, na mesma cadeia de interceptors que todo o resto, então um método de recurso pode assumir que suas entradas são estruturalmente válidas e se concentrar nas regras de negócio. Validação de valores de retorno (@Valid no método) existe, mas raramente compensa o custo.

Filtros, interceptors e outros providers

O comportamento transversal fica nos providers:

  • ContainerRequestFilter roda antes do método de recurso — autenticação, IDs de correlação, rate limiting. Anote-o com @PreMatching para rodar antes do roteamento (necessário para reescrever o método ou a URI) ou deixe-o post-matching para que possa inspecionar o recurso escolhido via ResourceInfo. Interrompa a requisição com requestContext.abortWith(Response.status(401).build()).
  • ContainerResponseFilter roda depois — headers de CORS e de segurança, logging das respostas.
  • ReaderInterceptor / WriterInterceptor envolvem os streams da entidade — compressão, assinatura, criptografia.
  • MessageBodyReader / MessageBodyWriter adicionam media types que o runtime não conhece, como CSV ou Protocol Buffers.
  • ParamConverterProvider ensina o runtime a converter parâmetros para seus próprios tipos, como um Money ou um OrderId.

Por padrão, os filtros valem globalmente. Para aplicá-los a métodos específicos, defina um name binding: uma anotação meta-anotada com @NameBinding, colocada tanto no filtro quanto nos métodos de recurso que precisam dele, de modo que @Secured em um método liga o filtro de autenticação só ali. A ordem entre filtros é definida com @Priority e as constantes de jakarta.ws.rs.Priorities (AUTHENTICATION roda antes de AUTHORIZATION, seguido de HEADER_DECORATOR, ENTITY_CODER e USER).

A segurança em si é delegada: com o Jakarta Security configurado no servidor, @RolesAllowed em um método de recurso (um enterprise bean, ou um bean CDI nos servidores que suportam isso) ou securityContext.isUserInRole("admin") a partir de um SecurityContext injetado faz a verificação; em APIs baseadas em token, um filtro de requisição valida o bearer token e instala um SecurityContext próprio. O guia de JWT cobre o lado dos tokens.

A API cliente

O Jakarta REST inclui um cliente fluente para chamar outros serviços, que também é a base do Rest Client type-safe do MicroProfile:

import jakarta.ws.rs.client.*;
import jakarta.ws.rs.core.*;

Client client = ClientBuilder.newBuilder()
        .connectTimeout(2, TimeUnit.SECONDS)
        .readTimeout(5, TimeUnit.SECONDS)
        .build();                                    // heavyweight: create once, reuse, close at shutdown

WebTarget orders = client.target("https://shop.example.com/api/orders");

try (Response response = orders.path("{id}").resolveTemplate("id", 42)
                               .request(MediaType.APPLICATION_JSON)
                               .header("Authorization", "Bearer " + token)
                               .get()) {
    if (response.getStatus() == 200) {
        OrderDetail order = response.readEntity(OrderDetail.class);
    }
}

OrderDetail created = orders.request()
        .post(Entity.json(new NewOrder("C-17", List.of(new Line("SKU-1", 2)))), OrderDetail.class);

WebTarget é imutável e reutilizável; Invocation.Builder define os headers de cada chamada. Ler a entidade diretamente (get(OrderDetail.class)) lança WebApplicationException em status de erro, o que é prático em scripts e um incômodo em serviços — prefira ler a Response e tratar os status explicitamente. Sempre configure timeouts: na maioria das implementações, o padrão é infinito. O cliente suporta os mesmos filtros e providers que o servidor, além da invocação reativa (.rx().get(...), que retorna um CompletionStage).

Respostas assíncronas e Server-Sent Events

Um método de recurso pode liberar a thread do contêiner enquanto espera: declare um parâmetro @Suspended AsyncResponse, retorne void e chame asyncResponse.resume(result) a partir de outra thread — um managed executor ou um message listener. setTimeout junto com um TimeoutHandler evita que requisições fiquem penduradas para sempre. Essa é a face Jakarta REST do processamento assíncrono de servlets, e só vale a pena quando a espera depende de algo externo.

Server-Sent Events transmitem uma sequência de eventos por uma única resposta de longa duração, o que atende bem notificações e atualizações de progresso sem precisar de WebSocket:

@GET
@Path("/{id}/events")
@Produces(MediaType.SERVER_SENT_EVENTS)
public void events(@PathParam("id") long id, @Context SseEventSink sink, @Context Sse sse) {
    subscriptions.register(id, status ->
        sink.send(sse.newEventBuilder().name("status").data(status).mediaType(MediaType.APPLICATION_JSON).build()));
}

SseBroadcaster distribui um mesmo evento para vários sinks. Lembre-se de que cada sink aberto é uma conexão mantida; configure um timeout de inatividade no load balancer e feche os sinks quando o cliente sumir.

Bootstrap no Java SE

O Jakarta REST 3.1 adicionou o SeBootstrap para que uma aplicação possa rodar sem contêiner de servlets: SeBootstrap.start(new ShopApplication(), SeBootstrap.Configuration.builder().port(8080).build()) sobe o servidor embarcado da implementação. É útil para ferramentas e testes, embora em um deploy Jakarta EE o contêiner de servlets continue sendo o host.

Erros comuns

  • Expor entidades JPA. Exceções de lazy loading durante a serialização, campos vazados e uma API com a cara do schema. Mapeie para DTOs.
  • Retornar 200 com corpo de erro. Os clientes não conseguem confiar nos códigos de status e o monitoramento não consegue contar as falhas. Lance e mapeie.
  • Mapper que captura tudo e devolve e.getMessage(). Vaza detalhes internos. Registre o erro no log do servidor e retorne um problem details genérico.
  • Nenhum timeout no cliente. Um serviço downstream travado esgota seu pool de threads. Configure timeouts de conexão e de leitura e considere circuit breakers.
  • Criar um Client por requisição. É caro e vaza conexões. Um por aplicação, fechado no shutdown.
  • @Path em uma classe sem escopo. Funciona, mas o ciclo de vida depende da implementação e a injeção CDI pode não funcionar. Declare @RequestScoped ou @ApplicationScoped.
  • Confiar em @Consumes para validar a entrada. Ele só verifica o media type. Adicione Bean Validation e verificações de negócio.
  • Usar GET para ações. Caches e mecanismos de prefetch vão acabar chamando o endpoint. Use POST para tudo o que tem efeitos colaterais.

Principais pontos

  • Uma aplicação com @ApplicationPath e classes de recurso com @Path formam uma API completa; o matching considera path, verbo e media types, nessa ordem, e incompatibilidades geram 404, 405, 415 ou 406 automaticamente.
  • Vincule as entradas com @PathParam, @QueryParam, @HeaderParam, @BeanParam e um corpo sem anotação; o JSON Binding cuida do JSON, e a 3.1 padronizou o multipart com EntityPart.
  • Retorne objetos para 200 simples e Response para todo o resto; use created(uri), noContent() e precondições em vez de montar status na mão.
  • Lance exceções e traduza-as em providers ExceptionMapper, de preferência para problem details da RFC 9457; nunca vaze mensagens internas.
  • Filtros, interceptors, readers, writers e conversores de parâmetros são os pontos de extensão; name bindings e @Priority controlam onde e em que ordem eles se aplicam.
  • A API cliente é fluente e reutilizável — um único Client por aplicação, timeouts explícitos e tratamento explícito dos status.
  • @Suspended AsyncResponse, Server-Sent Events e SeBootstrap cobrem esperas longas, streaming e uso standalone.

Fontes e leituras complementares

Perguntas frequentes

Qual implementação do Jakarta REST devo usar?
A que vem com o seu runtime: Jersey no GlassFish e no Payara, RESTEasy no WildFly e no Quarkus, a implementação do Liberty (baseada no RESTEasy) no Open Liberty e Apache CXF no TomEE. Código escrito seguindo a especificação roda em todas elas. Só escolha uma implementação deliberadamente para uso standalone, no Tomcat ou com a API de bootstrap do Java SE.
Como o Jakarta REST transforma meu objeto em JSON?
Por meio de um MessageBodyWriter. Os servidores Jakarta EE fornecem um baseado em JSON Binding (JSON-B), então qualquer classe com getters ou campos públicos é serializada automaticamente, e anotações como @JsonbProperty e @JsonbTransient permitem personalizar o resultado. Deploys standalone precisam adicionar um provider JSON por conta própria (Yasson para JSON-B, ou Jackson).
Um método de recurso deve retornar a entidade ou uma Response?
Retorne a entidade quando o sucesso for sempre um 200 com corpo; o framework cuida da serialização e da negociação de conteúdo. Retorne uma Response quando precisar de outro status, de headers como Location ou de um resultado condicional. Nunca retorne uma Response só para embrulhar um erro: lance uma exceção e deixe um ExceptionMapper gerar o corpo de erro de forma consistente.
Jakarta REST é a mesma coisa que o @RestController do Spring?
Mesma ideia, com anotações e contêiner diferentes. Os dois mapeiam métodos HTTP e paths para métodos Java, vinculam parâmetros e serializam JSON. Jakarta REST é o padrão implementado por vários fornecedores e roda em qualquer servidor Jakarta EE; o Spring MVC é do próprio Spring. Runtimes MicroProfile como Quarkus e Helidon usam Jakarta REST, o que o torna o conhecimento mais portável entre stacks Java.
Jakarta Servlet 6: servlets, filtros, sessões e a camada webComo o contêiner de servlets trata o HTTP: ciclo de vida, requisição e resposta, filtros, listeners, sessões, modo assíncrono e novidades do Servlet 6.0.Iniciante10 min de leituraEnterprise Beans 4: session beans stateless, stateful e singletonO que os session beans EJB oferecem (pooling, transações, segurança, timers, chamadas assíncronas), como diferem e quando um bean CDI é a melhor escolha.Intermediário10 min de leituraArquiteturaDesign de APIs REST: recursos, semântica HTTP, versionamento e formatos de erroComo projetar APIs HTTP que envelhecem bem: modelagem de recursos, métodos e status, paginação, versionamento, erros RFC 9457 e ETags.Intermediário11 min de leituraWeb e mobileCódigos de status HTTP: referência completa com significado e uso corretoTodos os códigos de status HTTP de 100 a 511: o que significam, os pares que confundem, redirecionamento, cache e orientações para design de APIs.Iniciante17 min de leituraSegurançaProtegendo APIs REST com JWT: verificação, claims e revogaçãoVerifique JWTs com segurança: estrutura JWS, allow-list de algoritmos, rotação de JWKS, validação de claims, revogação, armazenamento e defesas da API.Avançado9 min de leituraSpringComo construir APIs REST com @RestController no Spring MVCAPIs JSON com @RestController, ResponseEntity e Jackson: validação, erros RFC 9457 com @RestControllerAdvice e chamadas a serviços com RestClient.Intermediário6 min de leitura