Beliebte Suchanfragen
|
//

A microservice with Kotlin and Ktor - without Spring

14.6.2022 | 4 minutes of reading time

Ktor (see https://ktor.io/) is a Kotlin framework that provides both client and server functions and primarily uses the Kotlin DSL instead of annotations.

In this article, I would like to give an introduction to Ktor's server capabilities with a short example of a REST-Service. And that without the panacea Spring! :)

Let's not only talk about some dry code. For what purpose should I use this code? Well, my suggestion is to build a microservice.

What is a microservice, actually?

The term microservice was already mentioned several times, and it is certainly to be understood and interpreted in different ways. I would like to avoid this conceptional discussion as it is beyond the scope of this post.I'd rather tell you more about what I mean by that at this point:

In this context, ‚my‘ microservice is a server that is as compact as possible with some externally accessible REST interfaces that can be configured or monitored by external systems. Regarding its health status, I think of container orchestration tools, like Docker – or Prometheus, respectively Micrometer. The compactness described refers primarily to the focus of this single task: manage user data.

No more but also no less. This allows this component to be tested in a very targeted automated manner and software quality to be maintained at a high level. Or in other words: this microservice remains easily maintainable.

Manage user information, okay. Meaning?

Our user-application consists of a controller and a service. The controller usually provides various endpoints and calls the service for operations, e.g. to request or save user information.

Implementation

To specify our endpoints, we need to use the DSL 'routing' where we are able to define the HTTP-method and the logic we want to execute if a matching request comes in.

1routing {
2    get("/") {
3        call.respondText("Hello World!")
4    }
5}

Additionally so-called features can be accessed by using modules and, for example, enable the handling of exception thrown in our code. Furthermore, the feature 'metrics' is pretty interesting: In many cases, I would like to supervise my microservice e.g. with Prometheus. Along with common JVM information, we are able to implement and provide custom metrics:

1install(MicrometerMetrics) {
2    registry = appMicrometerRegistry
3}
4
5Metrics.addRegistry(appMicrometerRegistry)
6//...//
7Metrics.counter("add.user.successfull").increment()
8//...//
9
10get("/metrics") {
11call.respond(appMicrometerRegistry.scrape())
12}

Common micrometer supporting scraper, e.g. Prometheus, shouldn't have issues understanding this:

# HELP ktor_http_server_requests_seconds_max
# TYPE ktor_http_server_requests_seconds_max gauge
ktor_http_server_requests_seconds_max{address="localhost:8082",method="GET",route="/hello/{username}",status="400",throwable="n/a",} 0.116123034
# HELP ktor_http_server_requests_seconds
# TYPE ktor_http_server_requests_seconds summary
ktor_http_server_requests_seconds{address="localhost:8082",method="GET",route="/hello/{username}",status="400",throwable="n/a",quantile="0.5",} 0.113246208
ktor_http_server_requests_seconds{address="localhost:8082",method="GET",route="/hello/{username}",status="400",throwable="n/a",quantile="0.9",} 0.113246208
ktor_http_server_requests_seconds{address="localhost:8082",method="GET",route="/hello/{username}",status="400",throwable="n/a",quantile="0.95",} 0.113246208
ktor_http_server_requests_seconds{address="localhost:8082",method="GET",route="/hello/{username}",status="400",throwable="n/a",quantile="0.99",} 0.113246208
ktor_http_server_requests_seconds_count{address="localhost:8082",method="GET",route="/hello/{username}",status="400",throwable="n/a",} 1.0
ktor_http_server_requests_seconds_sum{address="localhost:8082",method="GET",route="/hello/{username}",status="400",throwable="n/a",} 0.116123034
...

Speaking of... configuration

When creating microservices, it is useful to have the possibility to externally configure our service. First and foremost the configuration of the ports might be a very important thing. Ktor provides a comprehensible way how we can access those properties:

1user {
2    port = 8083
3    basepath = /user
4}

Testing

However, in order to ensure that we can continue to place the most error-free endpoints possible with our controller to the outside world, we need to take care of automated testing. To do this, we use withTestApplication and pass a reference to our test environment. We are already in position to write unit tests with various requests, payloads and whatever else is needed.

1@Test
2fun `GET returns 200 and increases success metrics if authenticated`() {
3    runBlocking {
4        withApplication(testEnv) {
5            handleRequest(
6                    HttpMethod.Get,
7                    "/user/test"
8            ) {
9                addHeader(HttpHeaders.Authorization, "Bearer 1234567")
10            }.apply {
11                assertEquals(HttpStatusCode.OK, response.status())
12                assertEquals(response.content, "Hello user test, your id is test")
13                assert(Metrics.counter("user.successfull").count().equals(1.0))
14            }
15        }
16    }
17}

It is also convenient that we are not only quickly able to find potential errors but that we can also test modules for metrics and error handling. In the example below, we verify that our metrics were increased by 1.0 in case of a successful request

Looking at the positive case, we also want to make sure that we return an HTTP-401 if no authentication token is contained:

1@Test
2fun `POST returns 401 if unauthorized`() {
3    withApplication(testEnv) {
4        handleRequest(
5                HttpMethod.Post,
6                "/user/test"
7        ).apply {
8            assertEquals(HttpStatusCode.Unauthorized, response.status())
9        }
10    }
11}

Conclusion

So, is it possible to build microservices with Kotlin and Ktor? Oh yeah.

And not only quickly and intuitively, but also immediately quality-assured. Annotations and naming conventions fortunately do not play an immediate role, even though they can certainly still provide one or the other added value. Also we do not have to think about beans or application contexts.

It is sufficient to focus on the needs from a requirements perspective and what is absolutely necessary: a monitorable, configurable rest controller and its interfaces. And by the way: the development is quite enjoyable, because we receive fast feedback with a startup time of less than a second.

Version 2.0 was released earlier this year and has announced to focus on this kind of approach. After the first bugfix versions, there will be another blog post that takes a look the necessary migration steps and other interesting changes.

|

share post

Likes

2

//

More articles in this subject area

Discover exciting further topics and let the codecentric world inspire you.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.