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 Router 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.0.0-SNAPSHOT")- 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.
Startup
IGuiceContext.instance()
ββ GuicedRestPreStartup (IGuicePreStartup β early init)
ββ RestServiceScannerConfig (IGuiceConfigurator β enables annotation scanning)
ββ RestModule (IGuiceModule β Guice bindings)
ββ GuicedRestRouterConfigurator (VertxRouterConfigurator, order 100 β logging + CORS)
ββ OperationRegistry (VertxRouterConfigurator, order 200 β route registration)
ββ GuicedRestHttpServerConfigurator (VertxHttpServerConfigurator β server-level hooks)
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.
| 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 |
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 Router (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);
}
}com.guicedee.rest
βββ com.guicedee.vertx.web (Vert.x Web + Router SPI)
βββ com.guicedee.client (GuicedEE SPI contracts)
βββ io.vertx.auth.common (Vert.x auth)
βββ jakarta.ws.rs (Jakarta REST API)
βββ jakarta.annotation (Jakarta security annotations)
βββ org.slf4j (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.