Quarkus REST
|
This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against. Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change. This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Quarkus REST (artifact quarkus-rest) is Quarkus’s own Jakarta RESTful Web Services implementation, built
directly on top of Vert.x rather than on a servlet container, and is the default and recommended way to expose
REST endpoints in every current Quarkus project. It implements the same
Jakarta RESTful Web Services annotations any JAX-RS developer
already knows (@Path, @GET, @Produces, and so on) while adding Quarkus-specific shorthand annotations,
build-time processing that removes most of JAX-RS’s runtime reflection, and a dispatch model tuned for Vert.x’s
event loop. This page covers resources and routing, blocking versus non-blocking dispatch, filters and
exception handling, multipart and file handling, CORS/HTTP configuration and serialization, and how to migrate
an existing RESTEasy Classic codebase onto it.
Extension setup
A new project that needs REST endpoints with JSON support pulls in quarkus-rest and quarkus-rest-jackson:
./mvnw quarkus:add-extension -Dextensions=rest,rest-jackson
which adds the following to pom.xml:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
quarkus-rest alone gives you routing and dispatch with no JSON provider registered; almost every real project
also adds quarkus-rest-jackson (or quarkus-rest-jsonb, covered under JSON-B, and
XML) so entity parameters and Response bodies serialize automatically.
Resources and routing
@Path and HTTP-method annotations
A resource class is any class annotated @jakarta.ws.rs.Path, whose methods are annotated with an HTTP-method
annotation to become resource methods:
package com.example.book;
import java.net.URI;
import java.util.List;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.PATCH;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
@Path("/books")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class BookResource {
@GET
public List<Book> list(@QueryParam("author") String author) {
return BookRepository.findAll(author);
}
@GET
@Path("/{id}")
public Book get(@PathParam("id") Long id) {
return BookRepository.findById(id);
}
@POST
public Response create(Book book) {
Book created = BookRepository.persist(book);
URI location = UriBuilder.fromResource(BookResource.class).path("/{id}").build(created.id);
return Response.created(location).entity(created).build();
}
@PUT
@Path("/{id}")
public Book update(@PathParam("id") Long id, Book book) {
return BookRepository.update(id, book);
}
@PATCH
@Path("/{id}")
public Book rename(@PathParam("id") Long id, @QueryParam("title") String title) {
return BookRepository.rename(id, title);
}
@DELETE
@Path("/{id}")
public Response delete(@PathParam("id") Long id) {
BookRepository.delete(id);
return Response.noContent().build();
}
}
@Path on the class establishes the base URI (/books), and @Path on a method appends to it (/books/{id});
a class with no @Path is not a JAX-RS resource at all. {id} is a path template variable, bound to a method
parameter by @PathParam.
Parameter injection: classic JAX-RS vs. Quarkus REST shorthand
Standard JAX-RS requires naming the source explicitly on every parameter: @PathParam, @QueryParam,
@HeaderParam, @FormParam, @CookieParam, @MatrixParam. Quarkus REST additionally offers shorthand
equivalents — @RestPath, @RestQuery, @RestHeader, @RestForm, @RestCookie, @RestMatrix — that infer
the name from the parameter itself, so the name never has to be repeated:
| Standard JAX-RS | Quarkus REST shorthand | Notes |
|---|---|---|
|
|
Binds from the path template variable matching the parameter’s own name; give |
|
|
Same inference rule, from the query string. |
|
|
Same inference rule; header names with characters that are not valid Java identifiers still need the explicit
standard form or an explicit value passed to |
|
|
Also usable on a |
|
|
Same inference rule, from a cookie. |
|
|
Same inference rule, from a URI matrix parameter. |
package com.example.book;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.jboss.resteasy.reactive.RestHeader;
import org.jboss.resteasy.reactive.RestPath;
import org.jboss.resteasy.reactive.RestQuery;
@Path("/books")
public class BookSearchResource {
@GET
@Path("/{id}/reviews")
public String reviews(@RestPath Long id, @RestQuery String language,
@RestHeader("X-Correlation-Id") String correlationId) {
return BookRepository.reviewsFor(id, language, correlationId);
}
}
Both styles are fully interchangeable and can be mixed on the same method; the shorthand annotations exist purely to remove repetition and are otherwise equivalent to their standard counterparts.
Request and response bodies
A method parameter with no parameter annotation at all is the entity parameter — the deserialized request
body, as book is in the create and update methods above. A method’s return type is serialized as the
response body the same way, unless it returns jakarta.ws.rs.core.Response (or Quarkus REST’s own
org.jboss.resteasy.reactive.RestResponse), in which case the method controls status code, headers, and body
explicitly:
package com.example.book;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.core.Response;
@Path("/books")
public class BookLookupResource {
@GET
@Path("/{id}")
public Response find(@PathParam("id") Long id) {
Book book = BookRepository.findById(id);
if (book == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
return Response.status(Response.Status.OK).entity(book).build();
}
}
RestResponse<T> is Quarkus REST’s typed alternative to Response, preferred when the entity type should stay
visible in the method signature instead of being erased to Object:
package com.example.book;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import org.jboss.resteasy.reactive.RestResponse;
@Path("/books")
public class BookTypedLookupResource {
@GET
@Path("/{id}")
public RestResponse<Book> find(@PathParam("id") Long id) {
Book book = BookRepository.findById(id);
if (book == null) {
return RestResponse.status(RestResponse.Status.NOT_FOUND);
}
return RestResponse.ok(book);
}
}
Content negotiation
@Produces and @Consumes (both jakarta.ws.rs.*) declare, at the class or method level, which media types a
resource method can emit and accept; a request’s Accept header is matched against @Produces to select among
several methods producing different representations of the same resource, and a request whose Content-Type
does not match any @Consumes value is rejected with 415 Unsupported Media Type before the method body ever
runs. A method with no @Produces/@Consumes of its own inherits whatever is declared on the class.
@Blocking vs. non-blocking dispatch
Quarkus REST dispatches every incoming request on a small pool of Vert.x event loop threads. By default, a
resource method runs directly on that event loop thread — fast, since there is no thread hand-off, but only
safe for code that never blocks (no JDBC call, no synchronous HTTP client, no Thread.sleep), because blocking
an event loop thread stalls every other request currently assigned to it.
Quarkus REST decides which threading model a given resource method uses from a combination of its return type and any explicit annotation:
| Return type / annotation | Dispatch |
|---|---|
Plain return type ( |
Dispatched to a worker thread automatically — Quarkus REST assumes a plain return type means the method may block, and moves it off the event loop before invoking it. |
|
Stays on the event loop — returning a reactive type is Quarkus REST’s signal that the method itself never blocks and only composes non-blocking operations (a reactive datasource client, another non-blocking call), so no thread hand-off is needed. |
|
Forces worker-thread dispatch explicitly, overriding the return-type heuristic — useful on a method that
returns |
|
Forces event-loop dispatch explicitly, overriding the return-type heuristic — only correct when the method body is verified never to block. |
package com.example.book;
import io.smallrye.common.annotation.Blocking;
import io.smallrye.common.annotation.NonBlocking;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
@Path("/books")
public class BookDispatchResource {
// Plain return type -> automatically dispatched to a worker thread.
@GET
@Path("/{id}/legacy-lookup")
public Book legacyLookup(@PathParam("id") Long id) {
return BookRepository.blockingFindById(id); // safe to block here
}
// Uni<T> -> stays on the event loop by default.
@GET
@Path("/{id}")
public Uni<Book> find(@PathParam("id") Long id) {
return BookRepository.reactiveFindById(id);
}
// Explicit override: force this Uni-returning method onto a worker thread.
@GET
@Path("/{id}/with-blocking-setup")
@Blocking
public Uni<Book> findWithBlockingSetup(@PathParam("id") Long id) {
AuditLog.blockingWrite("lookup requested for " + id); // blocking I/O, deliberately
return BookRepository.reactiveFindById(id);
}
// Explicit override: force this plain-return-type method to stay on the event loop.
@GET
@Path("/ping")
@NonBlocking
public String ping() {
return "pong";
}
}
Getting this wrong in either direction has a real cost: forcing a genuinely blocking method onto the event loop
(@NonBlocking used incorrectly) stalls unrelated requests sharing that loop, while forcing an already
non-blocking method onto a worker thread wastes a thread-pool hand-off for no benefit. Virtual threads
(@io.smallrye.common.annotation.RunOnVirtualThread) are a separate, complementary option for blocking code
that needs worker-thread semantics without exhausting a bounded platform-thread pool — covered in depth on
the observability page and the concurrency guides referenced there.
Exception mappers and error responses
An unchecked exception escaping a resource method is, by default, translated into a generic 500 Internal
Server Error with no response body detail. An jakarta.ws.rs.ext.ExceptionMapper<T> intercepts a specific
exception type and turns it into a deliberate, semantic response instead:
package com.example.book;
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 BookNotFoundExceptionMapper implements ExceptionMapper<BookNotFoundException> {
@Override
public Response toResponse(BookNotFoundException exception) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorPayload(exception.getMessage()))
.type(MediaType.APPLICATION_JSON)
.build();
}
}
@jakarta.ws.rs.ext.Provider registers the mapper on startup; Quarkus REST picks the most specific mapper
available for a given exception’s type hierarchy, so a mapper for a common base exception acts as a fallback for
its subclasses that have no mapper of their own.
Filters and interceptors
jakarta.ws.rs.container.ContainerRequestFilter and ContainerResponseFilter run before a resource method is
dispatched and after it returns, respectively — typical uses are correlation-id propagation, authentication
checks that reject before the method body runs, and response header injection:
package com.example.book;
import java.io.IOException;
import java.util.UUID;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.ext.Provider;
@Provider
@Priority(Priorities.HEADER_DECORATOR)
public class CorrelationIdFilter implements ContainerRequestFilter, ContainerResponseFilter {
private static final String HEADER = "X-Correlation-Id";
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String correlationId = requestContext.getHeaderString(HEADER);
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
}
requestContext.setProperty(HEADER, correlationId);
}
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
responseContext.getHeaders().add(HEADER, requestContext.getProperty(HEADER));
}
}
jakarta.ws.rs.ext.ReaderInterceptor and WriterInterceptor sit one layer closer to the wire, wrapping entity
(de)serialization itself rather than the request/response as a whole — reach for them only for cross-cutting
concerns tied specifically to the entity stream (compression, encryption of the body), not for anything a filter
already covers.
Multipart requests and file upload/download
A multipart file upload binds each part with @RestForm, using org.jboss.resteasy.reactive.multipart.FileUpload
for a file part:
package com.example.book;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.multipart.FileUpload;
@Path("/books/{id}/cover")
public class BookCoverUploadResource {
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
public void uploadCover(@RestForm String title, @RestForm FileUpload cover) {
BookRepository.storeCover(title, cover.uploadedFile(), cover.fileName(), cover.contentType());
}
}
FileUpload exposes the part’s temporary on-disk location (uploadedFile()), original file name, and content
type; Quarkus streams the upload straight to a temporary file rather than buffering it fully in memory, so this
scales to large files without extra configuration.
File download is a matter of returning the bytes with the right content type — either a java.io.File/
java.nio.file.Path return type (Quarkus REST streams it directly) or an explicit Response:
package com.example.book;
import java.nio.file.Path;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path("/books/{id}/cover")
public class BookCoverDownloadResource {
@GET
@Produces("image/png")
public Response downloadCover(@PathParam("id") Long id) {
Path file = BookRepository.coverPath(id);
return Response.ok(file)
.header("Content-Disposition", "attachment; filename=\"cover.png\"")
.build();
}
}
CORS and HTTP configuration
CORS
Cross-origin requests from a browser-based front end are rejected by default and must be explicitly allowed via
quarkus.http.cors.* properties in application.properties:
quarkus.http.cors=true
quarkus.http.cors.origins=https://app.example.com,http://localhost:3000
quarkus.http.cors.methods=GET,POST,PUT,PATCH,DELETE,OPTIONS
quarkus.http.cors.headers=Content-Type,Authorization,X-Correlation-Id
quarkus.http.cors.exposed-headers=X-Correlation-Id
quarkus.http.cors.access-control-max-age=24H
quarkus.http.cors.access-control-allow-credentials=true
origins accepts an exact list, or a regular expression prefixed with / and suffixed / for matching a whole
family of origins (e.g. every subdomain in a preview-deployment setup). Leaving quarkus.http.cors.origins
unset while quarkus.http.cors=true allows any origin, which is rarely the right choice outside local
development.
HTTP server configuration
The most commonly overridden quarkus.http.* properties:
quarkus.http.port=8080
quarkus.http.test-port=8081
quarkus.http.root-path=/api
quarkus.http.enable-compression=true
quarkus.http.compress-media-types=application/json,text/plain
quarkus.http.access-log.enabled=true
quarkus.http.access-log.pattern=combined
quarkus.http.port is the port used by the packaged application; quarkus.http.test-port is the separate port
used while ./mvnw test and continuous testing run, so a test run never collides with a quarkus dev instance
already listening on 8080. quarkus.http.root-path prefixes every resource’s @Path with a common base
(/api/books instead of /books) without editing every resource class. Access logging
(quarkus.http.access-log.*) writes one line per request in a configurable format, useful for request-level
auditing independent of application-level logging.
TLS — certificates, keystores, mutual TLS, and the quarkus.tls.* registry — is deliberately not repeated
here; see the dedicated security/TLS page for that.
Metrics and distributed tracing for REST endpoints are likewise covered on
the observability page via Micrometer and OpenTelemetry, not
documented again on this page.
Serialization: Jackson, JSON-B, and XML
Quarkus REST needs a JSON provider extension to serialize/deserialize JSON entities; exactly one of the two should be on the classpath at a time:
| Extension | Library | Notes |
|---|---|---|
|
The default choice for new projects — |
|
|
Jakarta JSON Binding (JSON-B) |
The Jakarta EE-standard alternative, using |
XML support is a separate extension, quarkus-rest-jaxb, layering standard jakarta.xml.bind.annotation.*
(JAXB) annotations onto entity classes and activated by @Produces/@Consumes declaring an XML media type
(MediaType.APPLICATION_XML) rather than by any code change to the resource method itself.
Bean Validation on resource methods
Constraint annotations from jakarta.validation.constraints.* on a resource method’s parameters or return value
are validated automatically once quarkus-hibernate-validator is present, rejecting an invalid request with
400 Bad Request before the method body runs:
package com.example.book;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
@Path("/books")
public class BookValidatedResource {
@POST
public Book create(@Valid Book book) {
return BookRepository.persist(book);
}
}
class Book {
@NotBlank
@Size(max = 200)
String title;
}
Bean Validation itself — constraint annotations, custom constraints, validation groups — is not re-documented here; see the Hibernate Validator page for that.
Request pipeline
The diagram below traces one request from the Vert.x event loop through to the response being written, including
the point where @Blocking dispatch (or the plain-blocking-return-type default) hands execution to a worker
thread instead of continuing on the event loop:
Migrating from RESTEasy Classic
RESTEasy Classic (artifact quarkus-resteasy, plus quarkus-resteasy-jackson/quarkus-resteasy-jsonb) is the
legacy JAX-RS implementation Quarkus shipped before Quarkus REST existed, built on the Undertow servlet
container. It still works and still receives bug fixes, but Quarkus REST is its replacement for every current
and new project: built directly on Vert.x rather than through a servlet abstraction, it has materially lower
per-request overhead, avoids the servlet-thread model entirely, and is the JAX-RS implementation Quarkus’s
virtual-thread support (@RunOnVirtualThread) and reactive routing are designed around. There is no scenario in
which starting a new project on RESTEasy Classic is preferable to starting it on Quarkus REST.
Migrating an existing RESTEasy Classic project is, for the large majority of applications, a dependency swap rather than a rewrite:
./mvnw quarkus:remove-extension -Dextensions=resteasy,resteasy-jackson
./mvnw quarkus:add-extension -Dextensions=rest,rest-jackson
Standard jakarta.ws.rs.* annotations (@Path, @GET, @Produces, and so on) are unaffected, since both
implementations honor the same Jakarta RESTful Web Services specification — most resource classes compile and
behave identically without any change. What to check specifically when migrating:
-
Any dependency on RESTEasy-Classic-specific (
org.jboss.resteasy., notorg.jboss.resteasy.reactive.) classes or SPIs — these have Quarkus REST equivalents but not always identical names or packages. -
Blocking assumptions baked into resource methods that relied on RESTEasy Classic always running on a servlet worker thread — Quarkus REST’s return-type-driven dispatch (this page’s <<`@Blocking` vs. non-blocking dispatch>> section) means a method that blocks must either return a plain type (dispatched to a worker thread automatically) or be marked
@Blockingexplicitly if it also returnsUni/Multi. -
Any direct use of Servlet APIs (
jakarta.servlet.*) inside a resource class — Quarkus REST does not run on a servlet container by default, so such code needs the separatequarkus-undertowextension kept alongside it, or to be rewritten against Vert.x’s own APIs.