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

@PathParam("id")

@RestPath

Binds from the path template variable matching the parameter’s own name; give @RestPath("id") explicitly only when the parameter name and the template variable differ.

@QueryParam("author")

@RestQuery

Same inference rule, from the query string.

@HeaderParam("X-Correlation-Id")

@RestHeader

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 @RestHeader.

@FormParam("email")

@RestForm

Also usable on a FileUpload parameter for multipart file parts — see Multipart requests and file upload/download.

@CookieParam("session")

@RestCookie

Same inference rule, from a cookie.

@MatrixParam("version")

@RestMatrix

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 (Book, List<Book>, Response, void, …​), no annotation

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.

io.smallrye.mutiny.Uni<T> or io.smallrye.mutiny.Multi<T>

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.

@io.smallrye.common.annotation.Blocking

Forces worker-thread dispatch explicitly, overriding the return-type heuristic — useful on a method that returns Uni<T> but still needs to run blocking setup code before subscribing.

@io.smallrye.common.annotation.NonBlocking

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

quarkus-rest-jackson

Jackson

The default choice for new projects — com.fasterxml.jackson.annotation.* annotations (@JsonProperty, @JsonIgnore, @JsonInclude, and so on) work directly on entity classes, and a com.fasterxml.jackson.databind.ObjectMapper bean can be customized via a io.quarkus.jackson.ObjectMapperCustomizer bean.

quarkus-rest-jsonb

Jakarta JSON Binding (JSON-B)

The Jakarta EE-standard alternative, using jakarta.json.bind.annotation.* annotations instead — the better fit only when JSON-B portability to another Jakarta EE runtime specifically matters; otherwise Jackson’s wider ecosystem and configurability make it the default recommendation.

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:

flowchart TD A[Vert.x event loop receives request] --> B[ContainerRequestFilter chain] B --> C{Dispatch decision:\nreturn type / @Blocking / @NonBlocking} C -->|Uni/Multi, no @Blocking\nor @NonBlocking forced| D[Resource method executes\non the event loop] C -->|plain blocking type,\nor @Blocking forced| E[Dispatch to worker thread pool] E --> F[Resource method executes\non a worker thread] D --> G[ContainerResponseFilter chain] F --> G G --> H[Entity serialized\ne.g. Jackson] H --> I[Response written to the client]

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., not org.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 @Blocking explicitly if it also returns Uni/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 separate quarkus-undertow extension kept alongside it, or to be rewritten against Vert.x’s own APIs.