Reactive Routes and the Vert.x HTTP Layer
|
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. |
Every HTTP-facing piece of Quarkus — Quarkus REST, reactive routes,
WebSockets Next — is a client of the same underlying Vert.x HttpServer and the same small pool of event-loop
threads. There is no independent "REST stack" and "reactive routes stack" running side by side; there is one
Vert.x HTTP layer, and each of those programming models is a different set of ergonomics laid on top of it. This
page covers the lowest-level of those models — reactive routes, direct Vertx/WebClient/EventBus access — and the HTTP-server-level configuration (the management interface) that sits underneath all of them.
The Vert.x layer underneath everything
Reactive routes: the lowest-level HTTP model
Reactive routes let a CDI bean method handle an HTTP route directly, with almost nothing between the incoming
request and your code — no JAX-RS resource class, no annotation-processing-heavy request pipeline, just a
@Route-annotated method invoked by Vert.x itself. Add the capability (it ships as part of the core
quarkus-vertx-http layer that Quarkus REST and every other HTTP extension already pull in, so most projects that
already have a REST extension need nothing extra; the explicit artifact exists for a project with no other HTTP
extension yet):
./mvnw quarkus:add-extension -Dextensions=reactive-routes
which adds:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-routes</artifactId>
</dependency>
A route is a plain CDI bean method annotated with @Route:
package com.example.routes;
import io.quarkus.vertx.web.Route;
import io.quarkus.vertx.web.Route.HttpMethod;
import io.vertx.ext.web.RoutingContext;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class PingRoute {
@Route(path = "/ping", methods = HttpMethod.GET, produces = "text/plain")
void ping(RoutingContext context) {
context.response().end("pong");
}
}
@Route accepts the pieces you would expect from a low-level HTTP mapping:
| Attribute | Meaning |
|---|---|
|
The path this route matches — either a concrete/parameterized path ( |
|
One or more |
|
Media-type matching against the request’s |
|
An explicit integer controlling the position of this route in Vert.x’s route-matching chain relative to other routes — lower values are tried first; matters once multiple routes or filters could match the same request. |
A route method’s signature is intentionally flexible: it can take a RoutingContext, return void and write to
the response itself (as above), or return a value — including a Mutiny Uni/Multi — that Quarkus serializes
for you:
package com.example.routes;
import io.quarkus.vertx.web.Route;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class GreetingRoute {
@Route(path = "/hello/:name", methods = Route.HttpMethod.GET, produces = "application/json")
Uni<String> hello(io.vertx.ext.web.RoutingContext context) {
String name = context.pathParam("name");
return Uni.createFrom().item("Hello, " + name + "!");
}
}
See Reactive Programming and Mutiny for Uni/Multi
fundamentals — they behave identically here as they do returned from a Quarkus REST resource method.
@RouteFilter: cross-cutting logic below JAX-RS
@RouteFilter registers a method that runs for every matching request, before (or, with a negative priority
convention reversed, after) the routes it applies to — the reactive-routes equivalent of a JAX-RS
ContainerRequestFilter, but running at the raw Vert.x level, ahead of any REST-specific processing, and applying
uniformly to reactive routes, Quarkus REST endpoints, and any other Vert.x Web route registered in the
application:
package com.example.routes;
import io.quarkus.vertx.web.RouteFilter;
import io.vertx.ext.web.RoutingContext;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class RequestTimingFilter {
@RouteFilter(100)
void addTimingHeader(RoutingContext context) {
long start = System.nanoTime();
context.addHeadersEndHandler(void_ -> {
long elapsedMicros = (System.nanoTime() - start) / 1_000;
context.response().putHeader("X-Response-Time-Micros", String.valueOf(elapsedMicros));
});
context.next();
}
}
The integer argument to @RouteFilter is the same kind of ordering value as @Route’s `order — lower runs
earlier. Because a filter method receives the raw RoutingContext, it can inspect or rewrite headers, short-
circuit the request entirely (by calling context.response().end(…) instead of context.next()), or attach
completion handlers — full control, at the cost of losing the JAX-RS-level conveniences (@Context-injected
JAX-RS types, JAX-RS exception mapping) a ContainerRequestFilter would have.
Direct RoutingContext access
Both @Route methods and @RouteFilter methods work directly against io.vertx.ext.web.RoutingContext — the
same object Vert.x Web itself uses — giving unmediated access to the raw request (context.request(), headers,
the request body as a Buffer) and response (context.response(), streaming writes, trailers) without any
JSON-binding or JAX-RS-annotation layer standing in between. This is the level to reach for whenever that
mediation is itself the obstacle.
When to reach for reactive routes instead of Quarkus REST
Quarkus REST remains the right default for JSON/REST APIs — see Quarkus REST Services for that whole model, including its own approach to reactive return types, content negotiation, and validation. Reach for reactive routes instead when one of these actually applies:
-
Very low-level control over the raw request/response — streaming a response body incrementally, reading headers or the body in a shape JAX-RS annotations do not model well, or building a protocol-agnostic gateway in front of several backends.
-
A non-JSON, non-REST protocol riding over HTTP — webhooks with bespoke framing, server-sent events assembled by hand, a custom binary-over-HTTP protocol — where JAX-RS’s resource-method/media-type model does not map cleanly onto what is actually being sent.
-
Extremely performance-sensitive paths that want to skip JAX-RS processing overhead — a route method is invoked essentially directly by Vert.x, with none of the resource-matching, parameter-injection, or interceptor-chain machinery JAX-RS runs on every request; worth the extra manual wiring only where that overhead is measured to matter.
-
Framework or extension authoring — building tooling that needs to sit below any particular request model (health checks, metrics endpoints, and Quarkus’s own Dev UI are themselves implemented this way).
Outside those cases, Quarkus REST’s higher-level model is almost always the better fit — less code, standard JAX-RS semantics, and better interoperability with the rest of the Jakarta EE ecosystem.
Accessing the Vert.x API directly
Reactive routes are one way to sit close to Vert.x; the extensions below let any CDI bean — a route, a
Quarkus REST resource, a plain service — reach the underlying Vert.x APIs directly. Add quarkus-vertx for
this:
./mvnw quarkus:add-extension -Dextensions=vertx
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx</artifactId>
</dependency>
Injecting the core Vertx instance
Quarkus manages exactly one io.vertx.core.Vertx instance per application and makes it injectable anywhere:
package com.example.vertx;
import io.vertx.core.Vertx;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class VertxAwareService {
@Inject
Vertx vertx;
void scheduleCleanup() {
vertx.setTimer(5_000, id -> System.out.println("Running delayed cleanup"));
}
}
Everything else in this section — WebClient, the EventBus — is reached either from this same Vertx
instance or injected directly, and both approaches end up sharing the identical underlying event loop that
reactive routes and Quarkus REST run on.
WebClient: outbound HTTP at the Vert.x level
The MicroProfile REST Client remains the right default for calling a well-known, typed remote API — see
the REST Client page for that model. The Vert.x WebClient is the
lower-level alternative: useful when the target URL, method, or payload shape is dynamic, when the call needs to
happen from code that has no interface to declare a REST Client against, or when building a generic HTTP
proxy/gateway. Inject it directly:
package com.example.vertx;
import io.smallrye.mutiny.Uni;
import io.vertx.core.buffer.Buffer;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.HttpResponse;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class DynamicHttpCaller {
@Inject
WebClient webClient;
Uni<String> fetch(String host, int port, String path) {
return webClient.get(port, host, path)
.send()
.onItem().transform(HttpResponse::bodyAsString);
}
Uni<HttpResponse<Buffer>> postJson(String host, int port, String path, Buffer jsonBody) {
return webClient.post(port, host, path)
.putHeader("Content-Type", "application/json")
.sendBuffer(jsonBody);
}
}
quarkus-vertx already brings io.vertx.ext.web.client.WebClient transitively via the Vert.x Web Client
libraries; no separate artifact is needed beyond the extension added above. Because WebClient calls return
Mutiny types when used through Quarkus’s Vert.x integration, they compose with the same operators covered in
Reactive Programming and Mutiny.
EventBus: in-process messaging
The Vert.x EventBus is an in-JVM publish/subscribe and point-to-point message bus, reachable from any bean:
package com.example.vertx;
import io.vertx.mutiny.core.eventbus.EventBus;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class OrderEventPublisher {
@Inject
EventBus eventBus;
void orderPlaced(String orderId) {
// fire-and-forget publish to every consumer of this address
eventBus.publisher(String.class).send("orders.placed", orderId);
}
io.smallrye.mutiny.Uni<String> requestReplyExample(String orderId) {
// request/reply: wait for exactly one consumer's reply
return eventBus.<String>request("orders.validate", orderId)
.onItem().transform(io.vertx.mutiny.core.eventbus.Message::body);
}
}
@ConsumeEvent declares the other side — a plain bean method invoked whenever a message lands on an address,
with no manual bus lookup required:
package com.example.vertx;
import io.quarkus.vertx.ConsumeEvent;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class OrderValidationConsumer {
@ConsumeEvent("orders.placed")
void onOrderPlaced(String orderId) {
System.out.println("Order placed: " + orderId);
}
@ConsumeEvent("orders.validate")
String onValidate(String orderId) {
// the returned value becomes the reply for a request/reply call
return orderId.startsWith("ORD-") ? "valid" : "invalid";
}
}
By default a @ConsumeEvent method runs on an event-loop thread if the enclosing bean is not annotated
@Blocking, matching the split covered in the diagram above; add @Blocking on the consumer method (or the
bean) to have Quarkus dispatch it to the worker thread pool instead, exactly as it would a blocking Quarkus REST
resource method.
|
The Vert.x event bus in Quarkus is local and in-JVM by default — messages never leave the process. Clustering
the event bus so multiple application instances share one bus requires explicitly configuring a clustered Vert.x
setup, and is uncommon in typical Quarkus deployments: most projects that need cross-instance messaging reach for
a real broker instead — see Messaging for Kafka/AMQP via reactive messaging — rather than clustering the event bus itself. Treat clustering as an advanced, explicit opt-in, not a default
scaling path for |
The HTTP vs. management interface split
By default, every HTTP-facing concern — the application’s own REST/reactive-route endpoints, and Quarkus’s
built-in operational endpoints (health checks, metrics, the Dev UI in dev mode) — share the same Vert.x
HttpServer and port. Quarkus can instead expose the operational endpoints on a separate, dedicated management
interface: its own host/port, with its own HttpServer, isolated from the public application port entirely.
This is the recommended production pattern, since it means a load balancer, firewall rule, or network policy can
expose the application port publicly while keeping the management port reachable only internally.
Enable it and give it its own port:
quarkus.management.enabled=true
quarkus.management.port=9000
quarkus.management.host=0.0.0.0
Once quarkus.management.enabled=true, the built-in endpoints that support the management interface move off
the main application port onto this one automatically — most notably:
| Endpoint | Behavior once the management interface is enabled |
|---|---|
SmallRye Health ( |
Served from the management interface instead of the application port. |
Metrics ( |
Served from the management interface instead of the application port. |
Dev UI ( |
Remains reachable during dev mode; check the Management Interface Reference guide for the exact current behavior against the Quarkus version in use, since this is the one endpoint most likely to have version-specific nuances. |
A reactive route or @RouteFilter can itself be bound to the management interface rather than the main
application port, by setting its own management-specific configuration — useful for an internal-only diagnostic
route that should never be reachable from the public port at all. Consult the Management Interface Reference
guide linked below for the exact property names, since they are the kind of detail most likely to shift between
minor Quarkus releases.