Vert.x Web API Contract extends Vert.x Web to support OpenAPI 3, bringing to you a simple interface to build your router and mount security and validation handler.
If you are interested in building an application that routes API Requests to event bus, check out Vert.x Web API Service
To use Vert.x API Contract, add the following dependency to the dependencies section of your build descriptor:
Maven (in your pom.xml
):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-api-contract</artifactId>
<version>3.8.4</version>
</dependency>
Gradle (in your build.gradle
file):
dependencies {
compile 'io.vertx:vertx-web-api-contract:3.8.4'
}
Vert.x provides a validation framework that will validate requests for you and will put results of validation inside a container. To define a HTTPRequestValidationHandler
:
HTTPRequestValidationHandler validationHandler = HTTPRequestValidationHandler.create().addQueryParam("parameterName", ParameterType.INT, true).addFormParamWithPattern("formParameterName", "a{4}", true).addPathParam("pathParam", ParameterType.FLOAT);
Then you can mount your validation handler:
router.route().handler(BodyHandler.create());
router.get("/awesome/:pathParam")
// Mount validation handler
.handler(validationHandler)
//Mount your handler
.handler((routingContext) -> {
// Get Request parameters container
RequestParameters params = routingContext.get("parsedParameters");
// Get parameters
Integer parameterName = params.queryParameter("parameterName").getInteger();
String formParameterName = params.formParameter("formParameterName").getString();
Float pathParam = params.pathParameter("pathParam").getFloat();
})
//Mount your failure handler
.failureHandler((routingContext) -> {
Throwable failure = routingContext.failure();
if (failure instanceof ValidationException) {
// Something went wrong during validation!
String validationErrorMessage = failure.getMessage();
}
});
If validation succeeds, It returns request parameters inside RequestParameters
,
otherwise It will throw a fail inside RoutingContext
with 400 status code and ValidationException
failure.
Every parameter has a type validator, a class that describes the expected type of parameter.
A type validator validates the value, casts it in required language type and then loads it inside a RequestParameter
object. There are three ways to describe the type of your parameter:
There is a set of prebuilt types that you can use: ParameterType
You can instantiate a custom instance of prebuilt type validators using static methods of ParameterTypeValidator
and then load it into HTTPRequestValidationHandler
using functions ending with WithCustomTypeValidator
You can create your own ParameterTypeValidator
implementing ParameterTypeValidator
interface
Now you can handle parameter values:
RequestParameters params = routingContext.get("parsedParameters");
RequestParameter awesomeParameter = params.queryParameter("awesomeParameter");
if (awesomeParameter != null) {
if (!awesomeParameter.isEmpty()) {
// Parameter exists and isn't empty
// ParameterTypeValidator mapped the parameter in equivalent language object
Integer awesome = awesomeParameter.getInteger();
} else {
// Parameter exists, but it's empty
}
} else {
// Parameter doesn't exist (it's not required)
}
As you can see, every parameter is mapped in respective language objects. You can also get a json body:
RequestParameter body = params.body();
if (body != null) {
JsonObject jsonBody = body.getJsonObject();
}
A validation error fails the RoutingContext
with 400 status code and ValidationException
failure.
You can manage these failures both at route level using failureHandler
or at router level using errorHandler
:
router.get("/awesome/:pathParam")
// Mount validation handler
.handler(validationHandler)
//Mount your handler
.handler((routingContext) -> {
// Your logic
})
//Mount your failure handler to manage the validation failure at path level
.failureHandler((routingContext) -> {
Throwable failure = routingContext.failure();
if (failure instanceof ValidationException) {
// Something went wrong during validation!
String validationErrorMessage = failure.getMessage();
}
});
// Manage the validation failure for all routes in the router
router.errorHandler(400, routingContext -> {
if (routingContext.failure() instanceof ValidationException) {
// Something went wrong during validation!
String validationErrorMessage = routingContext.failure().getMessage();
} else {
// Unknown 400 failure happened
routingContext.response().setStatusCode(400).end();
}
});
Vert.x allows you to use your OpenAPI 3 specification directly inside your code using the design first approach. Vert.x-Web API Contract provides:
OpenAPI 3 compliant API specification validation with automatic loading of external Json schemas
Automatic request validation
Automatic mount of security validation handlers
You can also use the community project vertx-starter
to generate server code from your OpenAPI 3 specification.
You can create your web service based on OpenAPI 3 specification with OpenAPI3RouterFactory
.
This class, as name says, is a router factory based on your OpenAPI 3 specification.
OpenAPI3RouterFactory
is intended to give you a really simple user interface to use OpenAPI 3 related features. It includes:
Async loading of specification and its schema dependencies
Mount path with operationId or with combination of path and HTTP method
Automatic generation of validation handlers
Automatic conversion between OpenAPI style paths and Vert.x style paths
Lazy methods: operations are mounted in declaration order inside specification
Automatic mount of security handlers
To create a new router factory, use method OpenAPI3RouterFactory.create
.
As location It accepts absolute paths, local paths and local or remote URLs (HTTP or file protocol).
For example to load a spec from the local filesystem:
OpenAPI3RouterFactory.create(vertx, "src/main/resources/petstore.yaml", ar -> {
if (ar.succeeded()) {
// Spec loaded with success
OpenAPI3RouterFactory routerFactory = ar.result();
} else {
// Something went wrong during router factory initialization
Throwable exception = ar.cause();
}
});
You can also construct a router factory from a remote spec:
OpenAPI3RouterFactory.create(
vertx,
"https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/petstore.yaml",
ar -> {
if (ar.succeeded()) {
// Spec loaded with success
OpenAPI3RouterFactory routerFactory = ar.result();
} else {
// Something went wrong during router factory initialization
Throwable exception = ar.cause();
}
});
Or, you can also access a private remote spec by passing one or more AuthorizationValue:
AuthorizationValue authorizationValue = new AuthorizationValue()
.type("header")
.keyName("Authorization")
.value("Bearer xx.yy.zz");
List<JsonObject> authorizations = Collections.singletonList(JsonObject.mapFrom(authorizationValue));
OpenAPI3RouterFactory.create(
vertx,
"https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/petstore.yaml",
authorizations,
ar -> {
if (ar.succeeded()) {
// Spec loaded with success
OpenAPI3RouterFactory routerFactory = ar.result();
} else {
// Something went wrong during router factory initialization
Throwable exception = ar.cause();
}
});
You can also modify the behaviours of the router factory with RouterFactoryOptions
.
Now load your first operation handlers.
To load an handler use addHandlerByOperationId
.
To load a failure handler use addFailureHandlerByOperationId
You can, of course, add multiple handlers to same operation, without overwrite the existing ones.
For example:
routerFactory.addHandlerByOperationId("awesomeOperation", routingContext -> {
RequestParameters params = routingContext.get("parsedParameters");
RequestParameter body = params.body();
JsonObject jsonBody = body.getJsonObject();
// Do something with body
});
routerFactory.addFailureHandlerByOperationId("awesomeOperation", routingContext -> {
// Handle failure
});
Now you can use parameter values as described above
A security handler is defined by a combination of schema name and scope. You can mount only one security handler for a combination. For example:
routerFactory.addSecurityHandler("security_scheme_name", securityHandler);
You can of course use included Vert.x security handlers, for example:
routerFactory.addSecurityHandler("jwt_auth", JWTAuthHandler.create(jwtAuthProvider));
When you generate the Router
the Router Factory fails if For debugging/testing purpose
Router Factory automatically mounts a default handler for operations without a specified handler.
This default handler fails the routing context with 501 Not Implemented
error.
You can enable/disable it with setMountNotImplementedHandler
and you can customize this error handling with errorHandler
Router Factory automatically mounts a ResponseContentTypeHandler
handler when contract requires it.
You can disable this feature with setMountResponseContentTypeHandler
If you need to access to your operation contract while handling the request,
you can configure the router factory to push it inside the RoutingContext
with setOperationModelKey
. For example:
options.setOperationModelKey("operationPOJO");
routerFactory.setOptions(options);
// Add an handler that uses the operation model
routerFactory.addHandlerByOperationId("listPets", routingContext -> {
io.swagger.v3.oas.models.Operation operation = routingContext.get("operationPOJO");
routingContext
.response()
.setStatusCode(200)
.setStatusMessage("OK")
// Write the response with operation id "listPets"
.end(operation.getOperationId());
});
Router Factory automatically mounts a BodyHandler
to manage request bodies.
You can configure the instance of BodyHandler
(e.g. to change upload directory) with setBodyHandler
.
multipart/form-data
validationThe validation handler separates file uploads and form attributes as explained:
If the parameter doesn’t have an encoding associated field:
If the parameter has type: string
and format: base64
or format: binary
is a file upload with content-type application/octet-stream
Otherwise is a form attribute
If the parameter has the encoding associated field is a file upload
The form attributes are parsed and validated as other request parameters, while for file uploads the validation handler just checks the existence and the content type.
If you need to mount handlers that must be executed for each operationin your router before the operation specific handlers, you can use addGlobalHandler
Handlers are loaded by the router factory in this order:
Body handler
Custom global handlers
Global security handlers defined in upper spec level
Operation specific security handlers
Generated validation handler
User handlers or "Not implemented" handler (if enabled)
When you are ready, generate the router and use it:
Router router = routerFactory.getRouter();
HttpServer server = vertx.createHttpServer(new HttpServerOptions().setPort(8080).setHost("localhost"));
server.requestHandler(router).listen();
This method can fail with a RouterFactoryException
if you didn’t provide the required security handlers.
The Web API Contract provides an rxified version of APIs
Here is a complete example:
OpenAPI3RouterFactory
.rxCreate(vertx, "src/main/resources/petstore.yaml")
.flatMap(routerFactory -> {
// Spec loaded with success. router factory contains OpenAPI3RouterFactory
// Set router factory options.
RouterFactoryOptions options = new RouterFactoryOptions().setOperationModelKey("openapi_model");
// Mount the options
routerFactory.setOptions(options);
// Add an handler with operationId
routerFactory.addHandlerByOperationId("listPets", routingContext -> {
// Handle listPets operation
routingContext.response().setStatusMessage("Called listPets").end();
});
// Add a security handler
routerFactory.addSecurityHandler("api_key", JWTAuthHandler.create(jwtAuth));
// Now you have to generate the router
Router router = routerFactory.getRouter();
// Now you can use your Router instance
HttpServer server = vertx.createHttpServer(new HttpServerOptions().setPort(8080).setHost("localhost"));
return server.requestHandler(router).rxListen();
})
.subscribe(httpServer -> {
// Server up and running
}, throwable -> {
// Error during router factory instantiation or http server start
});