Building simple Webservices in Kotlin using Ktor

Caique Garutti Moreira
PlayKids Tech Blog
Published in
7 min readDec 24, 2019
Ktor by JetBrains

When you are developing a software product, be it a mobile application or a Web page, you will probably find yourself developing some Webservices to support it. APIs for authenticating users, sending them messages, serving media content, generating analytics, processing purchases, you name it! Webservices work as bridges between the pretty interfaces you show to your users and the complex code you maintain at your backend. You want your Webservices to be simple, you want them to be beautiful, you want them to be Ktor.

Ktor is a Kotlin framework for creating asynchronous Webservices. Before digging into some concepts and examples, I want to highlight the aspects of Ktor that made me chose it over other options:

  • Asynchronous behavior implemented using Kotlin's coroutines: Ktor handles async processing without excessive nestings due to the use of coroutines (more on Coroutines here)
  • Rich DSL built over Kotlin's Type-Safe Builders: Ktor's configuration of server capabilities and routing is done in a very idiomatic way, which makes code easy to read and understand. Even someone who doesn't know Ktor will be able to navigate through the files and find what they are looking for. Adding new APIs is also easier to do thanks to the DSL
  • No need for excessive annotations: You know what I'm talking about, right?
  • Easy way for intercepting requests and responses and extending behavior: With Ktor it's very easy to intercept requests in order to convert JSON strings to data classes, verify authentication headers, log requests, etc. You can also intercept responses to add custom headers or enrich the response body. All of that can be done without having to make all of your API extend a base class or implement a common interface.
  • Low technology constraints: Ktor doesn't impose constraints on core features such as dependency injection, logging, and serialization. You can choose to host Ktor on different servlet containers such as Tomcat and Glassfish or decide to use standalone servers such as Netty and Jetty

Now, for a very quick start, I want to show you how a simple Health API looks like on Ktor. You can find this example on this repository on GitHub. I decided to run it over a Netty server, so my Gradle dependencies are the following:

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"

implementation "io.ktor:ktor-server-core:$ktorVersion"
implementation "io.ktor:ktor-server-netty:$ktorVersion"
}

For this example, I'm using KtorVersion 1.2.6. The full code for configuring the server and handling the health API is the following:

These 7 lines are enough for configuring, instantiating and running a Ktor application that can process GET requests to the /health API! Try making a request to http://localhost:8383/health and you will see the magic!

Since we want to build more complex WebServices, it's time I introduce you some key concepts of Ktor:

Application:

One Ktor Application is a running instance of a Ktor server. It is composed of a set of modules (like the routing module shown on the example above). Each module is either a function or a lambda. An application instance is configured by defining a set of features that will be installed and by interceptions that will be applied to call pipelines.

ApplicationCall:

The Ktor ApplicationCall is an object that holds the Request and Response pair of a server call, alongside with associated parameters, allowing you to process them.

To receive the request body you may call ApplicationCall.receive<T>(), and to send the response you may call ApplicationCall.respond(Any). The desired application call is implicitly available inside the functions that handle or intercept the requests, such as get(), post(), put(), etc. I will show the usage of these methods on the next examples.

ApplicationCallPipeline:

In Ktor, all of the Http calls pass through a set of configured interceptors that can act on requests/responses and perform additional behavior before/after the call is processed by the target API. Each interceptor on an ApplicationCallPipeline has the power to either proceed() to the next interceptor in the chain or to finish() the entire pipeline.

Features:

Features are functionalities that can be added to a Ktor application. A feature behaves as an interceptor of the application call, defining behavior that will be applied to the call pipeline.

When you install a feature, its behavior will be applied to all the application calls. There are a lot of default Ktor features that can be installed when configuring one application, and you can also create your own features. Some common examples are:

  • Content Negotiation: Used for automatically converting (serializing/deserializing) requests and responses. For example, you can configure it to automatically convert JSON requests to your Kotlin data classes, and do the opposite for your responses. One option for doing that is using the Jackson converter
// on build.gradle add the dependency
implementation "io.ktor:ktor-jackson:$ktorVersion"
....// inside your Ktor application install the feature
install(ContentNegotiation) {
jackson
{
this.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
this.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
this.propertyNamingStrategy = PropertyNamingStrategy.SNAKE_CASE
}
}
// configuring the routes
routing {
post
("/post") {
val
request = call.receive(RequestDataClass::class)
val response = buildResponse(request)
call.respond(response)
}
}
  • Logging Requests/Responses: You can add an interceptor that will log all the requests and responses. This can be useful for debugging
  • Metrics and Analytics: You could intercept all the calls and measure the numbers of Http 500 errors, for example. You could also keep a set of histograms indicating the amount of time needed to process requests for each of your APIs
  • Authentication: One common use case is intercepting all the calls to protected APIs, checking authentication headers, validating tokens and then blocking or allowing access.

Routing:

If you understood the previous concepts, you will now be able to understand that the API declarations inside the routing{} function are nothing more than Application Call Interceptors that will only operate on the pipeline when the target path (URL) matches what is defined on the route. Let's get back to the Health API example:

get("/health") { call.respondText("I'm a healthy server") }

This is an idiomatic way of performing this:

intercept {
val uri = call.request.uri
when {
uri == "/health" -> call.respondText("I'm a healthy server")
}
}

Routes are declared on a tree fashion, which helps us writing idiomatic RESTful APIs:

Important: You don't need to declare all of your APIs directly on the file where you configure your Ktor application. You may declare them on separate files by defining extension functions to the Route class and passing them on the configuration of the Ktor application. We will see how to do it in our next section.

Photo by Fotis Fotopoulos on Unsplash

Putting it all together: Creating an Account Server

We've gone through the main concepts of Ktor WebServices, and now it is time to use them. In this example, we are going to implement a simple account server, that should provide the following APIs:

  • SignUp (POST to "/auth/sign-up"): Receives email and password, and creates an account using them
  • SignIn (POST to "/auth/sign-in"): Receives email and password, and uses them to authenticate and obtain a valid authentication token
  • Update Account Profile (PUT to "/account/{accountId}/profile"): Receives an account profile and updates it. It will only process requests if a valid authentication token is sent
  • Retrieve Account profile (GET to "/account/{accountId}/profile"): Will fetch the profile associated with the sent account id. It will only process requests if a valid authentication token is sent

The focus of this example is to show Ktor capabilities, so instead of using a real database all of the persistence is done through objects in memory. The full code of the sample can be found at https://github.com/KIQ83/ktor-examples/tree/master/ktor-sample-account-handling

I split the Configuration of the project, the Main class, and the Ktor server configuration into three different classes, for better readability. The configuration of the Ktor server looks like this:

There are interesting features being installed here, but before talking about them, let's focus on the routing part. As mentioned before, I decided to write my APIs on separate files, and I'm using them inside the routing{} function. Let's take a look at the AuthenticationRoute.kt file:

The signUp and signIn APIs are entirely defined in this separate class. As you can check, we are automatically parsing the JSON requests into instances of our Credential class. The responses are also being parsed automatically to JSON! On the signIn API, besides returning a response body, we are setting a custom header, with a generated authentication token. This token is a JWT that will be used for authenticating calls to the accountProfile APIs.

Before checking the AccountProfile API, let's focus on the authentication feature being installed on our Ktor Application, and on the authenticate function that encapsulates the account route.

install(Authentication) {
jwt {
realm = "sample ktor"
verifier(tokenService.jwtVerifier)
validate { credential ->
CustomPrincipal(UUID.fromString (credential.payload.subject))
}
}
}
routing {
route("api") {
auth(authenticationService)
authenticate {
account(accountProfileDAO)
}
}
}

What's happening here is that all of the calls inside the account route will be intercepted by our Authentication feature. This feature specifically is checking the Authorization header, retrieving a JWT token and trying to validate it using our custom verifier (tokenService.jwtVerifier). When the authentication token is missing or invalid, this interceptor will automatically throw an Http Status 401(Unauthorized) or 403 (Forbidden), preventing the request from being processed by the route. When the authentication token is present and valid, not only will the call be allowed to continue, but the Principal (composed of the logged accountId) will also be set to the call context, becoming available inside the AccountRoute APIs.

Aaand, that's it! Thanks for sticking together till the end! For more details over the examples, please clone this repository. Feel free to give your opinion about this article and ask questions about Ktor in the comments.

--

--