As you might know or have seen in my previous blog, the declaration of the routes and HTTP methods is done programmatically within Kotlin Ktor.
If you are used to specifying this information through annotation in Jakarta EE, it might feel a bit awkward. Although, due to the ‘trailing lambdas’ construct, this almost looks and feels like an annotation.
But actually, you can have something similar to annotations from the Jakarta EE world, with a little custom code.
Standard Ktor way
As mentioned, the trailing lambdas solutions allow you to define the routes in a very similar way to Jakarta EE. For each HTTP method, there exists a method with the same name that accepts 2 parameters. The part of the URL and a lambda which is executed when there is a request that matches the URL defined in the first parameter.
In this snippet, there are a few examples
get("/hello/{name?}") { val name = call.parameters["name"] ?: return@get call.respond( call.respond( HttpStatusCode.BadRequest, "Missing name parameter" ) ) val language = call.request.queryParameters["language"] ?: "en" call.respondText("Hello $name with language $language") } post("/person") { val person = call.receive<Person>() call.respondText("POST received with name ${person.name} and age ${person.age}") }
The required values from the request like path and query parameters or the body of the request can be retrieved from the call object passed into the lambda. This same object is also used to return the result to the client.
And this declaration, although programmatically and with no reflection or scanning required, is similar to the way we would define those methods in Jakarta EE.
@GET @Path("/hello/{name}") public String sayHello(@PathParam("name") String name, @QueryParam("language") String language) { }
Annotations solution
As mentioned in the introduction, with a little help of custom code, you can have a solution that uses annotations on functions and is very similar to the Jakarta EE approach.
Let us first look at an example
@GET("/{name}") suspend fun greeting(call: ApplicationCall, name: String, @DefaultValue("en") language: String?) { // function parameter names must match either variable name in URL pattern or is matched against query parameters call.respondText("Hello $name with language $language") } }
You can see that we define a method that has a GET annotation. The value of the annotation is a reference to a variable name that we also have as the function parameter name. But we can also retrieve query parameters by just defining them as parameters.
When we define a parameter as optional like in the example, the query parameter language
is not required in the URL. If we do not specify it as optional and the user calls the endpoint without specifying the query parameter, you get an exception.
In case the parameter is optional, we can define a default value by using the custom annotation @DefaultValue
as we see in the example.
Besides the possibility to catch path and query parameters as function parameters, we can also capture the body of the request. An example can be seen in the next snippet
@POST("/person") suspend fun savePerson(call: ApplicationCall, person: Person) { // The body can be retrieved by defining a function parameter. Conversion from JSON happens automatically. call.respondText("POST received with name ${person.name} and age ${person.age}") }
Here we capture the body of the Post request and convert the JSON to a Person instance.
In all cases, we also pass the ApplicationCall
instance to the method so that we can perform more logic based on request values and send the response back to the client.
The last thing we need to do is indicate where the methods can be found that have those annotations. Since Ktor doesn’t make use of a scanning mechanism, we need to provide the classes with those annotated functions.
The custom code wraps these functions in the regular Route definition of Ktor. This statement using the custom function processRoutes
processes the functions that have those custom annotations and makes it transparent for Ktor.
routing { processRoutes(HelloResource(), JsonResource()) }
The code is based on a gist from Thomas van den Bulk.
Conclusion
If you like to use the annotation-based configuration of endpoints in a similar way as Jakarta EE, you can use the code that is showcased in the project Ktor-annotated available within this GitHub repository.
A very small set of functions allow you to define annotations on Kotlin functions that define the Http method and call the function when a matching request comes in converting path and query parameters to the function parameters.
It can help those familiar with the way of defining REST endpoints within Jakarta EE but also simplifies handling path and query parameters for those familiar with Ktor.
Training and Support
Do you need a specific training session on Kotlin, Jakarta EE or MicroProfile? Or do you need some help in getting up and running with your next Ktor project? Have a look at the training support that I provide on the page https://www.atbash.be/training/ and contact me for more information.