REST APIs
View MarkdownVertique REST is standard JAX-RS annotations running on Vert.x, with the request pipeline wired for you:
- Standard annotations, non-blocking runtime. Write resources with
@Path,@GET,@POST, and@PathParam, a bounded, documented subset of JAX-RS 4.0 (see JAX-RS compatibility). - No reflection at request time. A compile-time processor registers every resource in a generated Dagger module and emits a per-method execution plan.
- Request validation by default. Bean Validation annotations become a JSON Schema that is validated on every request. Invalid input is rejected before your method runs, and you add no wiring.
- Automatic serialization. Return a plain value, a future, or a full
Response. JSON encoding, async resolution, and status handling are automatic. - A timeout at the resource boundary. A concrete Dagger-managed resource method can use
@Resilientwith@Timeoutwhen an inbound request must stop waiting for downstream work; the generated AOP proxy protects the asynchronous method execution. - Failures become RFC 9457 Problem Details. Exceptions and validation rejections come out as
consistent
problem+jsonresponses, and common exception types map to the right status with no code of yours. - OpenAPI on demand. Optionally publish an OpenAPI document for each declared application at runtime, built from the routes and request schemas it actually runs, or generate one at build time. Vertique’s model converters keep that spec faithful to your actual contract.
By the end of this page you will have extended the generated REST application from Quickstart with a resource method that takes a path parameter and a validated JSON request body, and you will understand how that method is registered, validated, serialized, and how its failures become HTTP responses.
Add a resource
Section titled “Add a resource”HelloResource, generated into rest-app/src/main/java/com/example/restapp/resource/HelloResource.java
by Quickstart, starts with a single @GET /hello method. Extend it with a path parameter and a
@POST method that accepts a JSON body, following the same @PathParam and bean-validation-annotated
request-body pattern used by the PostgreSQL REST archetype’s
ItemResource
and
CreateItemRequest:
@Path("/hello")public class HelloResource {
@Inject public HelloResource() {}
@GET @Produces(MediaType.APPLICATION_JSON) public HelloResponse hello() { return new HelloResponse("Hello, Vertique!"); }
@GET @Path("/{name}") @Produces(MediaType.APPLICATION_JSON) public HelloResponse helloTo(@PathParam("name") String name) { return new HelloResponse("Hello, " + name + "!"); }
@POST @Path("/greetings") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public HelloResponse createGreeting(CreateGreetingRequest request) { return new HelloResponse("Hello, " + request.name() + "!"); }
public record HelloResponse(String message) {}
public record CreateGreetingRequest(@NotBlank @Size(max = 255) String name) {}}@NotBlank and @Size are Jakarta Bean Validation annotations; the generated project already
depends on jakarta.validation:jakarta.validation-api transitively through vertique-starter-rest,
so no additional dependency is required. What consumes these annotations is covered in
Validate requests automatically below.
An operation’s id is used for routing and, when generated, for the OpenAPI document. It defaults to
the method name (hello, helloTo, createGreeting above). Set it explicitly with
@Operation(operationId = "...") when you want a stable id independent of the method name.
Register the resource with the generated Dagger module
Section titled “Register the resource with the generated Dagger module”You never add a resource to a Dagger module by hand. At compile time,
vertique-codegen-jaxrs’s annotation processor scans every @Path-annotated class and emits
GeneratedJaxRsResourcesModule, a @Module that provides the resource instance into the
@JaxRsResources multibinding RestModule reads at startup. This is the same generated module
AppComponent already lists, as shown in Application model:
@Modulepublic class GeneratedJaxRsResourcesModule { @Provides @ElementsIntoSet @JaxRsResources static Set<Object> helloResourceBinding(@VertxConfig JsonObject config, Provider<HelloResource> provider) { return Set.of(provider.get()); }}Extending HelloResource with the two new methods above does not change this generated module,
because one binding method covers every method on the resource class. It does add a generated
execution plan and descriptor per method, which is how the framework avoids reflecting on your
resource at request time. @NoAutoWire on a resource class opts it out of generation, so a
hand-written binding stays canonical.
Bound a resource method with a timeout
Section titled “Bound a resource method with a timeout”A JAX-RS resource is also a Dagger-managed bean, so the common resilience AOP path can wrap its
concrete methods. The useful resource-boundary case is a timeout: it gives an inbound request a
bounded wait while the resource performs downstream asynchronous work. @Resilient is the
method-level anchor and @Timeout supplies the bound. The method must return a parameterized
Future so the proxy can observe completion.
The following is an explanatory shape for a resource method; the request, response, and service are application code:
@Path("/payments")public class PaymentResource {
private final PaymentService paymentService;
@Inject public PaymentResource(PaymentService paymentService) { this.paymentService = paymentService; }
@POST @Resilient @Timeout(value = 5000) public Future<PaymentResponse> create(PaymentRequest request) { return paymentService.create(request); }}Install ResilienceModule, ResilienceAopModule, and the generated GeneratedAopModule together
with RestModule and GeneratedJaxRsResourcesModule. The generated resource binding obtains the
resource through a Dagger provider, allowing the AOP binding to substitute the proxy before the
resource is registered. This is the same substitution path exercised by the example’s
@RateLimited resource.
Authentication, content-type checks, and request validation remain route-level gates; the timeout
wraps the resource method and its downstream asynchronous work. Retry, circuit-breaker, and
bulkhead policy is not part of this inbound-resource example; place those concerns on an outbound
REST client or service operation where their failure and replay semantics are explicit. See
JAX-RS compatibility for the boundary
between standard JAX-RS and this Vertique extension.
Validate requests automatically
Section titled “Validate requests automatically”Request validation needs no wiring: vertique-starter-rest already includes
RestValidationModule. At startup it reads the same JAX-RS and Bean Validation annotations on
your resource methods and request bodies, including @NotBlank and @Size on
CreateGreetingRequest above, and synthesizes a JSON Schema for each operation, validated with
vertx-json-schema on every request. No OpenAPI document is involved or required.
A request that posts a blank name to /hello/greetings is rejected before createGreeting runs.
See Map failures to HTTP responses for the response body.
An opt-in alternative validates requests against a generated OpenAPI document instead. To use it,
set jaxrs.validationStrategy = "openapi-contract" and see the
vertique-rest-openapi-validation module reference.
Both strategies gate only the request. Neither validates responses.
Serialize the response
Section titled “Serialize the response”A resource method’s return value is serialized to JSON automatically; you never call an encoder
yourself. hello, helloTo, and createGreeting above all return a plain record, so each response
is 200 OK with a JSON body produced from that record and Content-Type: application/json. A
method may instead return a Future (resolved asynchronously, then serialized the same way); void,
or a Future that completes with no value (204 No Content); or a Response, plain or wrapped in a
Future, whose status, headers, and entity are taken directly from what you built.
By default the built-in vertique JSON profile serializes the response. A resource class or
method can select another named profile with @JsonProfile("...") for stricter or customized
behavior. JSON and profiles describes the built-in profiles and how to register your
own.
Map failures to HTTP responses
Section titled “Map failures to HTTP responses”An exception thrown from a resource method, or a request-validation rejection, flows through the
same error pipeline and comes out as an RFC 9457 Problem Detail JSON response, so you do not write a
try/catch around ordinary failures. Posting {"name":""} to /hello/greetings above produces
this response, 400 Bad Request with Content-Type: application/problem+json:
{ "type": "about:blank", "title": "Bad Request", "status": 400, "detail": "Request validation failed", "instance": "urn:uuid:5f1c3e0a-7b6d-4c1e-9d3a-2b8e4a7c9f10", "errors": [ { "pointer": "/name", "detail": "must have a minimum length of 1", "location": "body", "type": "minLength", "args": { "minLength": 1 } } ]}Each entry of errors locates the failure in one of two members. A failure in the JSON request body
reports a pointer, an RFC 6901 JSON Pointer over the names the client sent (/name, /items/0/name,
and "" for the whole body). A failure in any other request location, such as a query parameter, a
header or a form field, reports a parameter with its name (page, ids[1]). An entry carries one of
the two, or neither when the location cannot be stated, so test for the member’s presence rather than its
truthiness.
Every problem document that has no instance gets an opaque urn:uuid: occurrence id. The id is
random and is never taken from the request. The framework writes the same id on the log line of
the failure, so you can find the error in the server log from an id a client reports. An
exception mapper that sets its own instance keeps it.
Common exceptions (not-found, conflict, unauthorized, forbidden, unavailable, and more) already
map to the matching HTTP status without any code of yours. To map your own exception type,
contribute a jakarta.ws.rs.ext.ExceptionMapper for it through the Dagger
Set<ExceptionMapper<?>> multibinding, or customize the shared translator through the
RestExceptionMapperCustomizer multibinding. Both are covered in the
vertique-rest-jaxrs module reference.
Generate an OpenAPI document (optional)
Section titled “Generate an OpenAPI document (optional)”Nothing above requires an OpenAPI document: request validation runs from your annotations. When your API’s consumers want a contract, two independent producers can give them one, and you can use either or both:
- Publish at runtime when the document should describe what the deployed application actually
accepts.
vertique-rest-openapi-docsbuilds one document per declared application at startup and serves it over HTTP, to any caller or only to authorized ones, as described after the build-time setup below. - Generate at build time when you want the document as a build output, before anything runs, or when your resources are served without a declared application: the legacy default mount and a hand-built mount never get a runtime document.
The two documents are produced separately, and no parity between them is promised. At build time,
the standard swagger-maven-plugin-jakarta reads the same annotated resources, and Vertique’s
vertique-rest-openapi-plugin supplies the model converters that keep the generated spec faithful
to your actual contract instead of the framework’s wrapper types. FutureModelConverter unwraps
async return types to their resolved type, SseModelConverter resolves SSE streams,
BigDecimalModelConverter emits the strict string wire form, ScalarOptionalModelConverter
resolves scalar optionals, and RequestParamsExtension expands aggregated parameter objects into
individual OpenAPI parameters.
Add a vertique.version property to pom.xml (matching the framework revision you generated
against) and this plugin block, reconciled from
vertique-example-hello’s build configuration:
<plugin> <groupId>io.swagger.core.v3</groupId> <artifactId>swagger-maven-plugin-jakarta</artifactId> <configuration> <outputFileName>openapi</outputFileName> <outputPath>${project.build.directory}/classes</outputPath> <outputFormat>JSONANDYAML</outputFormat> <resourcePackages> <package>com.example.restapp.resource</package> </resourcePackages> <prettyPrint>true</prettyPrint> <modelConverterClasses> dev.vertique.openapi.FutureModelConverter </modelConverterClasses> </configuration> <dependencies> <dependency> <groupId>dev.vertique</groupId> <artifactId>vertique-rest-openapi-plugin</artifactId> <version>${vertique.version}</version> </dependency> </dependencies> <executions> <execution> <phase>compile</phase> <goals> <goal>resolve</goal> </goals> </execution> </executions></plugin>List the converters your API needs under modelConverterClasses. Running mvn -ntp compile with
this plugin in place writes openapi.json and openapi.yaml under target/classes, with one
path entry per resource method. The operationId of POST /hello/greetings resolves to
createGreeting, matching the fallback described above.
Publish documents at runtime. Add vertique-rest-openapi-docs. Its version comes from the
framework BOM, which the generated project’s parent already imports, so the dependency carries none,
as in the apidocs example’s
pom.xml:
<dependency> <groupId>dev.vertique</groupId> <artifactId>vertique-rest-openapi-docs</artifactId></dependency>Then list OpenApiDocsModule in your component beside RestModule and the generated resources
module, as the example’s
AppComponent
does (imports, Javadoc, and the deployment and lifecycle modules elided):
@VertiqueApp@Singleton@Component( modules = { VertxModule.class, ConfigParsingModule.class, RestModule.class, RestValidationModule.class, JwtAuthModule.class, OpenApiDocsModule.class, // … deployment and lifecycle modules … AppModule.class, GeneratedJaxRsResourcesModule.class })interface AppComponent extends VertiqueApplicationComponent {}A document exists for each declared application whose interface carries @ApiDocs, and who may
read it is decided in code, never in configuration: @ApiDocs(policy = DocsPolicy.class) names an access policy,
the same kind of typed policy a resource method uses (see Security). A policy that is
exactly @PermitAll serves the document to any caller and takes no securityScheme. Every other
policy serves it only to callers a named securityScheme authenticates and the policy then admits:
@Authorized for any authenticated caller, @RolesAllowed for listed roles, and so on. A policy that
is exactly @DenyAll also needs a scheme, and every reader is refused after authentication. The
example’s two declarations, shown under Several APIs, use a public policy and a
role-restricted one. @ApiDocs guards only the document routes; secure the application’s operations
themselves as usual.
Each generated document needs an info with a title and a version, taken from @OpenAPIDefinition
on the interface or from configuration. Entries under apidocs.documents, keyed by application
name, add that metadata and a server URL, as the example’s
application.json
does:
{ "apidocs": { "documents": { "public": { "serverUrl": "https://api.example.com/api/public" }, "management": { "info": { "title": "Management API", "version": "1.0" } } } }}Set "enabled": false in an application’s entry to switch its document off, or set
apidocs.enabled to false to switch every document off. An entry never switches a document on
without @ApiDocs, and an entry that tries to set access is refused at startup.
Each document is served as openapi.json and openapi.yaml under the apidocs.path prefix,
/apidocs by default, followed by the application’s name. The example serves
/apidocs/public/openapi.json and /apidocs/public/openapi.yaml to any caller, while
/apidocs/management/openapi.json and its YAML twin answer 401 without a token and 403 to a
token without the admin role.
A generated document is assembled at startup from the routes the application’s mount registers.
Under the default web-validation strategy, its request schemas are the ones the validation gate
enforces, less any reserved names it redacts, so the contract your consumers read is the one their
requests are checked against. An application that names its own contract, through openapiPath on
its @RestApplication, serves that contract as parsed instead of a generated document. The
vertique-rest-openapi-docs module reference
documents access, caching, document content, served contracts, and every configuration key in full.
Several APIs
Section titled “Several APIs”Every example so far runs behind one mount: either the legacy default mount RestModule builds at
jaxrs.basePath, or a hand-written recipe assembled from JaxRsRouterMount.Factory. To serve
several independent APIs, declare named applications: put @RestApplication on an interface, give
it a name and a path, and say which resources it serves. With no @RestApplication declared, none
of this applies and the default mount keeps working as shown above.
The apidocs example declares a public API and a management API side by side, in
PublicApi
and
ManagementApi
(license headers, packages, imports, and Javadoc elided):
@RestApplication( name = "public", path = "/api/public", resources = {CatalogResource.class})@ApiDocs(policy = PublicApi.DocsPolicy.class)@OpenAPIDefinition(info = @Info(title = "Catalog API", version = "1.0"))public interface PublicApi {
@PermitAll public interface DocsPolicy extends AccessPolicy {}}
@RestApplication( name = "management", path = "/api/mgmt", resources = {ManagementResource.class})@ApiDocs(policy = ManagementApi.DocsPolicy.class, securityScheme = "bearerAuth")public interface ManagementApi {
@RolesAllowed("admin") public interface DocsPolicy extends AccessPolicy {}}These two declarations mount CatalogResource under /api/public/* and ManagementResource under
/api/mgmt/*, and no default mount is created. @ApiDocs and @OpenAPIDefinition concern only
their OpenAPI documents (see
Generate an OpenAPI document), and the declaring interface
is never implemented or instantiated. vertique-codegen-jaxrs’s annotation processor checks each
declaration at compile time (its name, the path grammar, its membership, and the annotations the
interface may carry) and emits its registration into the generated
GeneratedJaxRsResourcesModule. A component that lists that module must therefore also list
RestModule, as every component serving REST traffic already does.
An application either lists its resources in resources, as both declarations above do, or sets
discover = true to receive every enabled resource, the common way to mount everything at one
prefix; vertique-example-services-codegen declares
@RestApplication(name = "services", path = "/", discover = true) in its
ServicesApi.
Discovery is allowed only for the sole declared application: beside a second declaration, active
or not, it fails.
Declaring any application switches the whole deployment into explicit mode, even when every
declared application is currently inactive: nothing is mounted implicitly. A resource contributed
by a hand-written @Provides method is routed only when an active application selects it. This
includes a resource a sibling framework module contributes, the same way your own resources are.
Startup still succeeds if you forget one, but it logs one warning naming every enabled resource
that no active application selected, so watch that warning the first time you add an application.
With no application active, the warning instead names the enabled generated resources
and states that manually contributed instances were not resolved. Startup also logs one
informational line listing every declaration and, per application mount, the resource classes it
received.
An application declared at path = "/" mounts at the whole path space, so it conflicts with every
other application and with any hand-built JAX-RS mount. Two active applications whose paths overlap
fail startup. If your deployment already serves its API at the root path and you want to add a
management application beside it, move the existing API under a non-root path first.
jaxrs.basePath does not apply once you declare any application, because each uses its own path
instead. Leaving a non-default jaxrs.basePath in place logs a warning that it is unused.
Declaring any application also widens the operation id check from one mount to every JAX-RS mount. Two distinct resource methods that resolve to the same operation id on different mounts fail startup before any route installs, naming both operations and both mounts. The same method of the same resource class keeps its id in every application that mounts it, so listing one resource class from two applications is fine, and an annotation-free proxy of a resource class counts as that class. Two resource classes that inherit one method from a shared base class are distinct owners and collide. Duplicate ids from distinct owners stay refused until a later change scopes operation ids per mount.
Each application mount also logs one warning per composition naming every operation that neither
restricts callers nor is declared public. An operation restricts callers when its effective security
policy denies all access, requires authentication, or restricts by role or scope; when it declares
one or more security requirements with no anonymous alternative among them; or when it has a
resolved @RequiresAction. @PermitAll marks an operation public only when nothing else already
restricts it. Set jaxrs.security.requireExplicitPolicy to true to turn that condition, on any
JAX-RS mount, application or not, into a startup failure instead.
A Jakarta Application subclass is not registered. If your code still has one (kept, for example,
from a Jakarta port), it compiles with one build warning naming it: its @ApplicationPath and
getClasses() have no effect, and its resources are served at jaxrs.basePath when no application
is declared, otherwise only where a declared application selects them. A build that treats warnings
as errors fails on it. Port it by declaring a @RestApplication interface with the same path that
lists its resources, or sets discover = true when it is the sole declaration, and then deleting
the subclass. @NoAutoWire on the subclass silences the warning while you migrate. Until it is
ported, nothing answers under its old @ApplicationPath prefix, so gateway routes, allowlists, and
rate limits keyed on that prefix stop matching its resources.
JAX-RS compatibility gives the verdict for
Application classes.
If two compilation units resolve their generated code to the same Java package, one unit’s registrations can silently never reach the component; give each unit a distinct package, or set the annotation processor’s package-override option. Framework and library modules keep contributing resources the way they always have and never declare an application themselves, because one declaration switches every consumer of that module into explicit mode.
The @RestApplication section of the vertique-rest-jaxrs module reference
documents membership, lifecycle, mount conflicts, and explicit mode in full, and its
leftover Application subclass section
covers migration. The vertique-codegen-jaxrs module reference covers the
compile-time declaration checks
and
porting a Jakarta Application subclass.
Learn more
Section titled “Learn more”vertique-rest-jaxrsmodule reference: resource scanning and registration, response serialization, and the full error pipeline.vertique-codegen-jaxrsmodule reference: the annotation processor that generatesGeneratedJaxRsResourcesModule, and the@NoAutoWireopt-out.vertique-resiliencemodule reference: the common resilience annotations, named policy tiers, and asynchronous execution pipeline.vertique-codegen-aopmodule reference: the generated proxy and Dagger substitution used by resource-level@Resilient.vertique-rest-validationmodule reference: the default annotation-driven JSON Schema request-validation strategy.vertique-rest-openapi-validationmodule reference: the opt-in strategy that validates against the generated OpenAPI document instead.vertique-rest-openapi-pluginmodule reference: the build-time OpenAPI model converters.vertique-rest-openapi-docsmodule reference: runtime OpenAPI documents for declared applications: access, configuration, document content, and served contracts.- Apidocs example
PublicApi: one of two declared applications in an example that publishes a public and a protected document. vertique-jsonmodule reference: named Jackson mapper profiles for request and response serialization.- PostgreSQL REST archetype
ItemResource: a full CRUD resource using the same path-parameter and validated-body pattern, backed by a repository. - Quickstart
- Application model
- Services: define a typed service contract and inject its generated client.
- What is Vertique?
Continue reading
Section titled “Continue reading”- Previous: Code generation
- Next: REST clients