Skip to content

REST APIs

View Markdown

Vertique 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 @Resilient with @Timeout when 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+json responses, 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.

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:

@Module
public 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.

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.

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.

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.

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.

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-docs builds 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.

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.