Overview
In this tutorial, we will set up a simple project based on the Knot.x Starter Kit template.
We will implement a custom handler that will print a "Hello world from Knot.x!" message.
Prerequisites
- JDK 8
- Linux or OSX bash console (for Windows users we recommend using e.g. Ubuntu with Windows Subsystem for Linux)
What you’re going to learn:
- How to use the Design First approach to design the API's contract first before writing any code configuring Open API specification entries
- How to implement a custom business logic, the Knot.x Handler, and apply it to configured API
Download the Latest Knot.x Starter Kit release and unzip it.
Please note that the support for ZIP distribution was added here. If your version does not contain those changes you can easily apply them with upgrade notes.
Project has the following structure:
├── docker
| ├── Dockerfile // Docker file with image definition. It will be not used in this tutorial.
├── functional // Keep here your functional tests. Example implementation included
├── gradle // Gradle wrapper and common gradle scripts
├── knotx/conf // Knotx configuration which will be copied to distribution ZIP
├── modules // Sub-modules of your project
│ ├── ... // example modules implementation
Configure API: specify path and method
When it comes to API development, two approaches are available: The “Design First” and the “Code First”. You can read more details here to understand the differences between them. Knot.x uses the Design First approach focusing on developer experience and API design quality. It uses the OpenAPI 3.0 specification to configure HTTP request routing.
The knotx/conf/openapi.yaml
file contains project-specific API definitions using the OpenAPI 3.0 specification syntax.
We can see three distinguishable parts of this file:
Information about the project, which is quite self-explanatory:
... openapi: "3.0.0" info: version: 1.0.0 title: Knot.x Starter Kit description: Knotx Starter Kit contact: name: "Knot.x Community" url: "https://knotx.io/community/" ...
Server configuration that defines how the server behaves:
... servers: - url: https://{domain}:{port} description: The local API server variables: domain: default: localhost description: api domain port: enum: - '8092' default: '8092' ...
Here we can see, for example, that the server will be registered on port
8092
. Notice variable substitution indicated by{
brackets}
. For more information on what can be configured here, see this documentation.Paths configuration that explicitly specifies the endpoints, their paths, methods, responses and much more in a clear, easily readable and declarative way:
... paths: ... /healthcheck: get: operationId: healthcheck-operation # https://vertx.io/docs/vertx-health-check/java/ responses: '200': description: Everything is fine '204': description: No procedure are registered. '501': description: At least one procedure has reported a non-healthy status '500': description: One procedure has thrown an error or has not reported a status in time ...
In OpenAPI terms, paths are endpoints (resources), such as
/users
or/reports/summary/
, that your API exposes, and operations are the HTTP methods used to manipulate these paths, such as GET, POST or DELETE. (read more here)
The Starter Kit template comes with example paths and methods defined. Let's see the /healthcheck
API. It is used to express the current state of the application in very simple terms: UP or DOWN (more on that in the second tutorial).
We can see that it is registered under /healthcheck
path, it's method is a GET
and it can have various responses depending on the state of the instance.
A configured path and method section declares a unique operation that is used to process matching requests. In the example above, it is healthcheck-operation
. Details how to link configured operation with the business logic implementation will be covered later.
Now, it is time to define a custom endpoint /api/hello
and operation hello-world-operation
.
Simply add new routing configuration under paths
:
paths:
/api/hello:
get:
operationId: hello-world-operation
responses:
default:
description: Example API implemented in this tutorial
Configure API: define a new operation
As previously mentioned, a configured path and method indicates operation to perform. Every operation is configured as a Routing Operation entry that specifies Handlers with business logic to execute and failure Handlers when processing fails.
All routing operation entries are configured in the knotx/conf/routes/operations.conf
file. It is the first time the Knot.x configuration appears. It uses the HOCON (Human-Optimized Config Object Notation) syntax. More on HOCON can be found in here.
Let's take a look at this file:
routingOperations = ${routingOperations} [
// routing operations for this project
]
routingOperations
is an array of routingOperation
objects.
${routingOperations}
takes care of merging any previously defined routing operations with the ones you define here. More about HOCON concatenation possibilities can be found here.
Let's take a look at an example routing operation entry we get from Starter Kit:
{
operationId = healthcheck-operation
handlers = [
{
name = healthcheck
}
]
}
The healthcheck-operation
operation defines the array of Handlers with a single Handler, named healthcheck
. Please note that Handlers can be easily reused so healthcheck
is a Handler's factory unique name.
Let's take a look at a more complicated one:
{
operationId = example-api-with-fragments-operation
handlers = ${config.server.handlers.common.request} [
{
name = singleFragmentSupplier
config = {
type = json
configuration {
data-knotx-task = api-task
}
}
},
{
name = fragmentsHandler
config = { include required(classpath("routes/handlers/api-with-fragments.conf")) }
},
{
name = fragmentsAssembler
}
] ${config.server.handlers.common.response}
}
Here we provide not one, but three different handlers. By defining them in an array, they will be called in order they're defined. Some of them even have some additional configurations defined explicitly or in another file. They will be passed as JsonObject
s to the handler implementation.
Notice two additional imports: ${config.server.handlers.common.request}
and ${config.server.handlers.common.response}
. Here we use the handlers defined in Knot.x source code, which simplifies handling of basic requests without repeating the configuration for similar operations.
Now, let's connect hello-world-operation
with a handler. The name of the handler factory will be hellohandler
(reminder: names must be unique!). We'll also make the message configurable.
Simply add new routing operation to the array:
{
operationId = hello-world-operation
handlers = [
{
name = hellohandler
config = {
message = "Hello World From Knot.x!"
}
}
]
}
API implementation: creating new Handler
Now it is time to code some custom logic printing "Hello World from Knot.x!". We will implement the RoutingHandlerFactory interface. We use Java but you can use any JVM-based language, such as Kotlin. Let's put it in a separate module.
Create a new module under the modules
folder. The structure should look like this:
modules
└─ hellomodule
├── build.gradle.kts // gradle build script
└── src
└── main
├── java/com/project/example/hellohandler
│ └── HelloWorldHandlerFactory.java // the handler factory
└── resources
└── META-INF/services
└── io.knotx.server.api.handler.RoutingHandlerFactory // META-INF file used by Knot.x to find the handler
Let's take a look at the three files required for the module to work.
build.gradle.kts
plugins {
`java-library`
}
dependencies {
"io.knotx:knotx".let { v ->
implementation(platform("$v-dependencies:${project.property("knotx.version")}"))
api("$v-fragments-api:${project.property("knotx.version")}")
api("$v-fragments-handler-api:${project.property("knotx.version")}")
}
"io.vertx:vertx".let { v ->
implementation("$v-web")
implementation("$v-web-client")
implementation("$v-rx-java2")
}
}
io.knotx.server.api.handler.RoutingHandlerFactory
io.knotx.example.hellohandler.HelloWorldHandlerFactory
In this file we list, line by line, all the RoutingHandlerFactory implementations we want Knot.x to see. It has to be a canonical class name. It uses Java ServiceLoader.
HelloWorldHandlerFactory.java
package io.knotx.example.hellohandler;
import io.knotx.server.api.handler.RoutingHandlerFactory;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.reactivex.core.Vertx;
import io.vertx.reactivex.ext.web.RoutingContext;
public class HelloWorldHandlerFactory implements RoutingHandlerFactory {
@Override
public String getName() {
return "hellohandler"; // this has to be exactly the same unique string as in operations.conf file
}
@Override
public Handler<RoutingContext> create(Vertx vertx, JsonObject config) {
String message = config.getString("message", "Some default message");
JsonObject jsonObject = new JsonObject()
.put("message", message);
return event -> event.response().end(jsonObject.toString());
}
}
The most important thing is to register it by its name. In this case, it was "hellohandler"
.
The handling it performs is simply ending the response with previously configured "Hello World from Knot.x!" JSON.
Finally, let's update the main project's settings.gradle.kts
file, so that it will recognize the new module.
Simply add those two lines:
include("hello-module")
and
project(":hello-module").projectDir = file("modules/hello-module")
Build & Run
The process of distributing Knot.x via stack is the following:
$ ./gradlew build
This command is building the distributable zip archive. It can be found under build/distributions
directory.
This archive is fully functional Knot.x distribution and can be deployed on any Unix-based machine.
In order to run it, first we need to unzip it.
$ cd build/distributions
$ unzip knotx-stack-<version of your project>.zip
Now let's navigate to unzipped knotx
folder and make knotx/bin/knotx
file executable.
$ cd knotx
$ chmod +x bin/knotx
The final step is executing the starting script:
$ bin/knotx run-knotx
The application should start.
After a while it should be up and running and the endpoint can be accessed. Let's execute the command:
$ curl -X GET http://localhost:8092/api/hello
{"message":"Hello World From Knot.x!"}
Debugging
For manual on how to debug the application see this documentation.
You can find full example implementation here.