Building simple Webservices in Kotlin using Ktor
By: Caique Garutti Moreira
Tags: Ktor, Playkids, Backend, Kotlin, Software Development

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

: 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

: 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

( to ): 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

( to ): 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

( to ): 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.