Categories
JAX-RS Ktor

Annotation-based definition of routes in Ktor

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.

Categories
JAX-RS Kotlin Ktor

Why don’t you create your next Backend application with Kotlin and Ktor?

For a long time, Kotlin was on my list of things I wanted to learn more about. In the second half of 2022, I finally got the chance to learn the language and started using Ktor, the ‘web application runtime’ for Kotlin.

Kotlin

Kotlin is a modern programming language that offers several benefits over Java. Firstly, Kotlin supports object-oriented, functional, and procedural programming constructs, making it a versatile language for a wide range of projects.
Secondly, Kotlin is fully interoperable with Java, which means that developers can easily use Kotlin in conjunction with existing Java codebases. Additionally, Kotlin offers support for several concepts, such as data classes, structured concurrency, and virtual threads, which were not available in Java until much later. Kotlin also offers null safety features, which help prevent common errors that can arise due to null pointer exceptions.
Another significant benefit of Kotlin is its compact and highly readable grammar, which makes code more concise and easier to understand. Finally, Kotlin offers extension functions, which allow developers to add new functions to existing classes, making it easier to write reusable code.

Kotlin is a modern, powerful, and highly expressive programming language that helps developers write better code faster and more efficiently.

We will encounter various powerful constructs of Kotlin in this blog series called “project FF”. In this Framework Face-off, I will compare several Java application runtimes, including Spring Boot and Quarkus, with Jakarta EE and how you implement Enterprise functionality.

But today, I give already two examples of the power of Kotlin.

Kotlin extension functions

Kotlin extension functions allow you to add new functions to an existing class without having to inherit from that class or modify its source code.
In other words, extension functions let you extend the functionality of a class in a more flexible and modular way. This can make your code more readable and easier to maintain.

Extension functions are similar to static utility methods, but with the added benefit of being able to call them as if they were instance methods of the class, they are extending. This can make your code more concise and easier to understand.

fun String?.hasSpaces(): Boolean {
   val found = this?.find { it == ' '}
   return found != null
}

fun main() {
   val data = "A sentence with spaces in of course"
   println(data.hasSpaces()) // True

   val data2 = "word"
   println(data2.hasSpaces()) // False
}

For a detailed explanation of this code, I refer you to the Kotlin tutorials. In short, the hasSpaces function is defined on a nullable String type. The function body checks if we find a space in the string value. From that point on, we can use the hasSpaces on a String as you can see in the main function.

Creating an interceptor

You can create an interceptor in plain Kotlin using lambdas and high-order functions. Functions and lambdas are first-class citizens and are treated equally as data objects.

inline fun timeBlock(block: () -> T): Pair {
   val startTime = System.nanoTime()
   val result = block()               // execute lambda
   val endTime = System.nanoTime()
   return Pair(result, endTime - startTime)
}

fun main() {
   val number = 121
   val (isPrimeValue, time) = timeBlock {
      (2..number/2).none { number % it == 0 }
   }

   println("Result of prime check for $number is $isPrimeValue ")
   println("Execution took $time nanoSeconds ")
}

Here we define a function that accepts a lambda and we time the execution of the lambda. The function returns the result of the lambda and the time it took.

As example, we time the calculation to check if a value is a prime value. Also here you can see the power of Kotlin but still keep it readable.

(2..number/2).none { number % it == 0 }

defines that we go from value 2 until the half of the value we need to check and there should be no value where the modulo of the value with the number is 0.

Ktor

Ktor is a lightweight, asynchronous web server that easily allows you to define what is needed. In the blog “Time is code, My friend”, Ktor performed really well and performed as good as Quarkus which has a very elaborate and complex pre-processing system to achieve such a fast startup.

Since it is highly modular, you can keep the runtime, and memory usage very low. And since there are modules available for each major topic or framework, you can create applications that integrate with them very easily.

Also, Ktor is created with Kotlin Coroutines support built-in. Kotlin Coroutines provide you with solutions for asynchronous non-blocking programming, structured concurrency and solutions similar to Java Virtual Threads. Things that will only appear in Java in the coming versions but are already available for many years within Kotlin.

Ktor Example

In this blog, I’ll show you how you can create an application with endpoints that handle JSON payload.

You can generate a Maven or Gradle project using the Ktor Project Generator web page or directly from within IntelliJ IDEA.

By default, the Netty engine is selected to handle the HTTP request handling but you can select other engines if you like.

Under the plugin section, make sure you select the ‘kotlinx.serialization‘ plugin to have support for JSON. This automatically brings in the routing plugin which is responsible for calling a certain Kotlin function for a URL pattern and the Content Negotiation plugin that sets header values accordingly to the requirements like JSON types.

The generated project contains a main function within the Application.kt file which starts Netty and an extension function that define the configuration of the modules. This allows for a clear separation of each functionality and quickly lets you find each setting.

To have full JSON support, we don’t need to configure anything, just activate the plugin. The Maven plugin in this case makes sure that any class that is annotated with @Serializable can be used at runtime without the need for any reflection. This makes the solution native compile friendly. This a topic we will discuss later in more detail.

For the routing, we call a function that corresponds to the HTTP method we want to support. It has a URL pattern and lambda expression as a parameter. This way, we can define the function for each URL in a very concise and readable way.

    get("/") {
        call.respondText("Hello World!")
    }

The entire example can be found at the Project FF Github repository: https://github.com/rdebusscher/Project_FF/tree/main/ktor-started/JAX-RS

Conclusion

In a time where modular runtimes that are small and take up little memory are important for some people, Kotlin and Ktor are ideal solutions. Kotlin offers many benefits over Java and offers a compact syntax but still highly readable. It supports many important features already for years that are introduced only recently or planned in Java.

Ktor is the natural solution if you choose Kotlin and need to write a backend application. Although you can combine Kotlin with Spring Boot and Quarkus, Ktor is a lightweight solution that has the same functionality available through its modules and is faster at runtime.

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.

This website uses cookies. By continuing to use this site, you accept our use of cookies.  Learn more