Lightweight Jakarta REST (JAX-RS) adapter for Vert.x 5 with full GuicedEE integration.
Annotate your classes with standard @Path, @GET, @POST, etc. β routes are discovered at startup via ClassGraph and registered on the Vert.x RouterConfig automatically. Resource instances are created through Guice, so @Inject works everywhere.
Built on Vert.x 5 Β· Jakarta REST Β· Google Guice Β· JPMS module com.guicedee.rest Β· Java 25+
<dependency>
<groupId>com.guicedee</groupId>
<artifactId>rest</artifactId>
</dependency>Gradle (Kotlin DSL)
implementation("com.guicedee:rest:2.4.0")- Zero-config route registration β
OperationRegistryscans for@Path-annotated classes and maps them to Vert.x routes at startup - Jakarta REST annotations β
@Path,@GET,@POST,@PUT,@DELETE,@PATCH,@HEAD,@OPTIONS,@Produces,@Consumes - Parameter binding β
@PathParam,@QueryParam,@HeaderParam,@CookieParam,@FormParam,@MatrixParam,@BeanParam - Guice-managed resources β resource classes are obtained from the Guice injector, so
@Injectjust works - Reactive support β methods can return
Uni<T>,Future<T>, or plain values; reactive types run on the event loop, blocking methods are dispatched to a worker pool @Verticleworker pools β resource classes in@Verticle-annotated packages automatically use their named worker pool- CORS β annotation-driven (
@Cors) or environment-variable-driven CORS handler configuration - Security β
@RolesAllowed,@PermitAll,@DenyAllwith pluggableAuthenticationHandlerandAuthorizationProvider - Exception mapping β
jakarta.ws.rs.ext.ExceptionMapperSPI + built-in status-code mapping with cause-chain traversal - JAX-RS
Responsesupport β returnjakarta.ws.rs.core.Responsefor full control over status, headers, and entity - Jackson serialization β request/response bodies are (de)serialized with Jackson via Vert.x JSON; configurable via the
DefaultObjectMapperbinding RestInterceptorSPI β hook into request start/end for logging, metrics, or cross-cutting concerns
Step 1 β Annotate a class with Jakarta REST annotations:
@Path("/hello")
public class HelloResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public String hello() {
return "Hello, world!";
}
}Step 2 β Bootstrap GuicedEE (routes are discovered and registered automatically):
IGuiceContext.registerModuleForScanning.add("my.app");
IGuiceContext.instance();That's it. OperationRegistry discovers HelloResource, registers GET /hello on the Vert.x router, and the endpoint is live.
flowchart TD
n1["Startup"]
n2["IGuiceContext.instance()"]
n1 --> n2
n3["GuicedRestPreStartup<br/>IGuicePreStartup β early init"]
n2 --> n3
n4["RestServiceScannerConfig<br/>IGuiceConfigurator β enables annotation scanning"]
n2 --> n4
n5["RestModule<br/>IGuiceModule β Guice bindings"]
n2 --> n5
n6["GuicedRestRouterConfigurator<br/>VertxRouterConfigurator, order 100 β logging + CORS"]
n2 --> n6
n7["OperationRegistry<br/>VertxRouterConfigurator, order 200 β route registration"]
n2 --> n7
n8["GuicedRestHttpServerConfigurator<br/>VertxHttpServerConfigurator β server-level hooks"]
n2 --> n8
HTTP Request
β Vert.x Router
β CORS handler (if configured)
β SecurityHandler (authentication + authorization check)
β OperationRegistry.handleRequest()
β vertx.runOnContext() β event-loop dispatch
β ParameterExtractor.extractParameters() β bind @PathParam, @QueryParam, body, etc.
β GuiceRestInjectionProvider.getInstance() β Guice-managed resource instance
β method.invoke(instance, params) β invoke the resource method
β ResponseHandler.processResponse() β Uni / Future / Response / sync result
β ResponseHandler.handleException() β on failure (ExceptionStatusMapper)
Execution is dispatched via vertx.runOnContext() to ensure the method runs on the Vert.x event-loop thread, keeping context-local state (e.g. Hibernate Reactive sessions, Mutiny subscriptions) properly associated with the current request.
Class-level @Path is combined with method-level @Path:
@Path("/api")
public class UsersResource {
@GET
@Path("/users/{id}")
public User getUser(@PathParam("id") Long id) {
return userService.find(id);
}
}GET /api/users/42
@ApplicationPath is also respected and prepended to @Path.
Path parameters use Jakarta REST {param} syntax β they are automatically converted to Vert.x :param style at registration time.
Within each resource, routes with more literal path characters register first. A
literal /buckets route therefore wins over /{resource} regardless of reflection order.
| Annotation | Source | Example |
|---|---|---|
@PathParam |
URL path segment | @PathParam("id") Long id |
@QueryParam |
Query string | @QueryParam("page") int page |
@HeaderParam |
HTTP header | @HeaderParam("Authorization") String auth |
@CookieParam |
Cookie value | @CookieParam("session") String session |
@FormParam |
Form field | @FormParam("username") String user |
@MatrixParam |
Matrix parameter | @MatrixParam("color") String color |
@BeanParam |
Composite bean | @BeanParam SearchCriteria criteria |
| (none) | Request body | Deserialized via Jackson |
@DefaultValue supplies a typed value when a bound parameter is absent. For example,
@QueryParam("limit") @DefaultValue("50") int limit uses 50 when the query omits
limit; a supplied value takes precedence and an invalid supplied number still fails.
All primitive types, boxed types, enums, UUID, BigDecimal, BigInteger, LocalDate, LocalDateTime, LocalTime, OffsetDateTime, ZonedDateTime, and Instant are converted automatically from string values.
For unknown types, the JAX-RS Β§3.2 fallback chain applies:
static valueOf(String)static fromString(String)- Single-
Stringconstructor
Complex body parameters are deserialized using the configured Jackson ObjectMapper (bound as DefaultObjectMapper in Guice).
If a method parameter is typed as RoutingContext, the current Vert.x routing context is injected directly:
@POST
@Path("/upload")
public String upload(RoutingContext ctx) {
return ctx.fileUploads().toString();
}Methods returning Uni<T> or Future<T> are treated as non-blocking and run on the event loop:
@GET
@Path("/reactive")
public Uni<List<Item>> listItems() {
return itemRepository.listAll();
}All other methods are treated as blocking and dispatched via vertx.executeBlocking():
@GET
@Path("/blocking")
public List<Item> listItemsBlocking() {
return itemRepository.listAllSync();
}If the resource class belongs to a package annotated with @Verticle that defines a named worker pool, blocking work is dispatched to that pool instead of the default:
@Verticle(workerPoolName = "rest-pool", workerPoolSize = 16, worker = true)
package com.example.api;All @Verticle are named, although their name goes into workerPoolName.
The @Produces annotation determines the response Content-Type. When absent, application/json is the default.
| Return type | Behavior |
|---|---|
null / void |
204 No Content |
String (text/plain) |
Written as UTF-8 text |
| Any object (JSON) | Serialized via Json.encodeToBuffer() (Jackson) |
byte[] |
Written directly as application/octet-stream |
jakarta.ws.rs.core.Response |
Status, headers, and entity are applied directly |
Uni<T> |
Subscribed per-request; cancelled on client disconnect |
Future<T> |
Completed per-request; discarded on client disconnect |
Return a Response for full control:
@POST
@Path("/items")
@Produces(MediaType.APPLICATION_JSON)
public Response createItem(Item item) {
Item created = itemService.create(item);
return Response.status(201)
.header("Location", "/items/" + created.getId())
.entity(created)
.build();
}| Annotation | Effect |
|---|---|
@RolesAllowed("admin") |
Requires authentication + role check |
@PermitAll |
No authentication required |
@DenyAll |
Always returns 403 |
Annotations are checked at method level first, then class level.
@Path("/admin")
@RolesAllowed("admin")
public class AdminResource {
@GET
@Path("/dashboard")
public String dashboard() {
return "admin-only";
}
@GET
@Path("/public")
@PermitAll
public String publicEndpoint() {
return "everyone";
}
}Register a default AuthenticationHandler and/or AuthorizationProvider:
SecurityHandler.setDefaultAuthenticationHandler(myAuthHandler);
SecurityHandler.setDefaultAuthorizationProvider(myAuthProvider);When a secured endpoint is hit:
- If no authenticated user is present β 401 Unauthorized
- If the user doesn't have the required role β 403 Forbidden
Apply @Cors at the class or method level:
@Path("/api")
@Cors(
allowedOrigins = {"https://example.com"},
allowedMethods = {"GET", "POST"},
allowCredentials = true,
maxAgeSeconds = 3600
)
public class ApiResource { ... }| Attribute | Default | Purpose |
|---|---|---|
allowedOrigins |
* |
Allowed origin patterns |
allowedMethods |
GET, POST, PUT, DELETE, PATCH, OPTIONS |
Allowed HTTP methods |
allowedHeaders |
Common REST headers | Allowed request headers |
allowCredentials |
true |
Whether credentials are allowed |
maxAgeSeconds |
3600 |
Preflight cache duration |
enabled |
true |
Enable/disable CORS |
All @Cors attributes can be overridden without code changes:
| Variable | Purpose |
|---|---|
REST_CORS_ENABLED |
Enable/disable CORS globally |
REST_CORS_ALLOWED_ORIGINS |
Override allowed origins |
REST_CORS_ALLOWED_METHODS |
Override allowed methods |
REST_CORS_ALLOWED_HEADERS |
Override allowed headers |
REST_CORS_ALLOW_CREDENTIALS |
Override credentials flag |
REST_CORS_MAX_AGE |
Override max age seconds |
| Exception | Status |
|---|---|
IllegalArgumentException |
400 |
IllegalStateException |
400 |
SecurityException |
403 |
NoResultException (JPA) |
404 |
WebApplicationException |
Uses embedded status |
NullPointerException |
500 |
Other RuntimeException |
500 |
Register custom exception mappers via ServiceLoader / JPMS provides:
public class CustomExceptionMapper implements ExceptionMapper<CustomException> {
@Override
public Response toResponse(CustomException e) {
return Response.status(422)
.entity(Map.of("error", e.getMessage()))
.build();
}
}module my.app {
provides jakarta.ws.rs.ext.ExceptionMapper
with my.app.CustomExceptionMapper;
}When an exception yields a generic 500 status, ExceptionStatusMapper walks the cause chain to find a more specific mapping β for example, a NoResultException wrapped inside a CompletionException still maps to 404.
| SPI | Purpose |
|---|---|
VertxRouterConfigurator |
Customize the Vert.x RouterConfig (CORS, logging, etc.) |
VertxHttpServerConfigurator |
Customize the Vert.x HttpServer |
VertxHttpServerOptionsConfigurator |
Customize HttpServerOptions |
jakarta.ws.rs.ext.ExceptionMapper |
Map exceptions to HTTP responses |
RestInterceptor |
Hook into request start/end lifecycle |
IGuiceConfigurator |
Configure classpath scanning |
IGuiceModule |
Contribute Guice bindings |
IGuicePreStartup |
Run logic before route registration |
Resource classes are obtained via IGuiceContext.get(Class), so standard Guice injection works:
@Path("/orders")
public class OrderResource {
private final OrderService orderService;
@Inject
public OrderResource(OrderService orderService) {
this.orderService = orderService;
}
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Order getOrder(@PathParam("id") Long id) {
return orderService.findById(id);
}
}flowchart LR
com_guicedee_rest["com.guicedee.rest"]
com_guicedee_rest --> com_guicedee_vertx_web["com.guicedee.vertx.web<br/>Vert.x Web + Router SPI"]
com_guicedee_rest --> com_guicedee_client["com.guicedee.client<br/>GuicedEE SPI contracts"]
com_guicedee_rest --> io_vertx_auth_common["io.vertx.auth.common<br/>Vert.x auth"]
com_guicedee_rest --> jakarta_ws_rs["jakarta.ws.rs<br/>Jakarta REST API"]
com_guicedee_rest --> jakarta_annotation["jakarta.annotation<br/>Jakarta security annotations"]
com_guicedee_rest --> org_slf4j["org.slf4j<br/>logging"]
Module name: com.guicedee.rest
The module:
- exports
com.guicedee.guicedservlets.rest.services,com.guicedee.guicedservlets.rest.pathing,com.guicedee.guicedservlets.rest.implementations - provides
IGuiceConfigurator,IGuicePreStartup,IGuiceModule,IPackageRejectListScanner,VertxRouterConfigurator,VertxHttpServerConfigurator - uses
VertxRouterConfigurator,VertxHttpServerConfigurator,VertxHttpServerOptionsConfigurator,ExceptionMapper,RestInterceptor
| Class | Package | Role |
|---|---|---|
OperationRegistry |
pathing |
Discovers @Path classes and registers Vert.x routes |
JakartaWsScanner |
pathing |
ClassGraph-based scanner for Jakarta REST resources |
PathHandler |
pathing |
Resolves @ApplicationPath + @Path into full route paths |
HttpMethodHandler |
pathing |
Maps Jakarta HTTP annotations to Vert.x HttpMethod |
ParameterExtractor |
pathing |
Extracts and converts method parameters from the request |
ResponseHandler |
pathing |
Converts method results (sync, Uni, Future, Response) into HTTP responses |
EventLoopHandler |
pathing |
Dispatches blocking vs. reactive methods to the correct thread |
SecurityHandler |
pathing |
Evaluates @RolesAllowed, @PermitAll, @DenyAll |
ExceptionStatusMapper |
pathing |
Maps exceptions to HTTP status codes |
CorsHandlerConfigurator |
implementations |
Configures Vert.x CORS handlers from @Cors and env vars |
GuicedRestRouterConfigurator |
implementations |
Wires logging and CORS onto the router |
GuicedRestHttpServerConfigurator |
implementations |
HTTP server-level customizations |
RestServiceScannerConfig |
implementations |
Enables rich ClassGraph scanning for REST discovery |
RestModule |
implementations |
Guice module for REST bindings |
GuiceRestInjectionProvider |
services |
Provides Guice-backed resource instances |
GuicedRestPreStartup |
services |
Pre-startup lifecycle hook |
RestInterceptor |
services |
SPI for request start/end interception |
Cors |
services |
Annotation for CORS configuration |
Issues and pull requests are welcome β please add tests for new parameter types, response writers, or security features.