DEV Community

Cover image for Ktor - a Kotlin web framework
Dan Newton
Dan Newton

Posted on • Originally published at lankydan.dev on

Ktor - a Kotlin web framework

Ktor is an asynchronous web framework written in and designed for Kotlin. Allowing the more impressive features of Kotlin, such as coroutines, to not only be used but supported as a first-class citizen. Typically, Spring is my go-to general framework and usually what I use when I need to put a REST API together. But, after recently attending a London Kotlin meetup where there was a presentation on Ktor, I decided I’d try something new for once. That is how I ended up here, writing a blog post about Ktor. So, this post is a learning experience for both you and me. The content of this post will lack experienced advice but will instead document my journey as I play around with Ktor for the first time.

Here is a bit more background information on Ktor. It is backed by Jetbrains who are also the creators of Kotlin itself. Who better to make a Kotlin web framework than the men and women that work on the language.

Implementation

Dependencies

buildscript {
  ext.kotlin_version = '1.3.41'
  ext.ktor_version = '1.2.2'

  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
  }
}

apply plugin: 'java'
apply plugin: 'kotlin'

// might not be needed but my build kept defaulting to Java 12
java {
  disableAutoTargetJvm()
}

// Ktor uses coroutines
kotlin {
  experimental {
    coroutines "enable"
  }
}

compileKotlin {
  kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
  kotlinOptions.jvmTarget = "1.8"
}

dependencies {
  // Kotlin stdlib + test dependencies

  // ktor dependencies
  compile "io.ktor:ktor-server-netty:$ktor_version"
  compile "io.ktor:ktor-jackson:$ktor_version"
  // logback for logging
  compile group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'
  // kodein for dependency injection
  compile group: 'org.kodein.di', name: 'kodein-di-generic-jvm', version: '6.3.0'
}
Enter fullscreen mode Exit fullscreen mode

A few things are going on here.

  • Ktor requires a minimum version of Kotlin 1.3, so that coroutines can be leveraged.

  • Dependencies on ktor-server-netty and ktor-jackson are brought in. As the name suggests, this means Netty will be used for this post. Different underlying web servers can be used depending on which you choose to import. Currently, the remaining options are Jetty and Tomcat.

  • Logback is brought in to handle logging. This is not included in the Ktor dependencies and is needed if you plan on doing any sort of logging.

  • Kodein is a dependency injection framework written in Kotlin. I have used it loosely in this post, and due to the size of the code examples, I could probably remove it altogether. The main reason it is there is to provide me with another chance to use something other than Spring. Remember this is also one of the reasons that I am trying out Ktor.

Starting the web server

With the boring stuff out of the way, I can now run you through implementing a simple web server. The code below is all you need:

fun main() {
  embeddedServer(Netty, port = 8080, module = Application::module).start()
}

fun Application.module() {
  // code that does stuff which is covered later
}
Enter fullscreen mode Exit fullscreen mode

Bam. There you have it. A simple web server running with Ktor and Netty. Ok, yes, it doesn’t really do anything, but we’ll expand on this in the following sections. The code is pretty self-explanatory. The only piece worth highlighting is the Application.module function. The module parameter of embeddedServer requires an extension function for Application. This is going to be the main function that makes the server do stuff.

In the following sections, we will expand the contents of Application.module so that your web server actually does something worthwhile.

Routing

At the moment, all incoming requests will be rejected since there are no endpoints to handle them. By setting up the routing, you can specify valid paths that requests can travel along and the functions that will process the requests when they reach their destinations.

This is done inside of a Routing block (or multiple Routing blocks). Inside of a block, routes to different endpoints are set up:

routing {
  // all routes defined inside are prefixed with "/people"
  route("/people") {
    // get a person
    get("/{id}") {
      val id = UUID.fromString(call.parameters["id"]!!)
      personRepository.find(id)?.let {
        call.respond(HttpStatusCode.OK, it)
      } ?: call.respondText(status = HttpStatusCode.NotFound) { "There is no record with id: $id" }
    }
    // create a person
    post {
      val person = call.receive<Person>()
      val result = personRepository.save(person.copy(id = UUID.randomUUID()))
      call.respond(result)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

routing is a little convenience function to make the code flow a little smoother. The context (a.k.a this) inside of routing is of type Routing. Furthermore, the functions route, get and post are all extension functions of Routing.

route sets a base path to all its following endpoints. In this scenario, /people. get and post do not specify a path themselves since the base path is suffice for their needs. If desired, a path could be added to each one, for example:

routing {
  // get a person
    get("/people/{id}") {
      val id = UUID.fromString(call.parameters["id"]!!)
      personRepository.find(id)?.let {
        call.respond(HttpStatusCode.OK, it)
      } ?: call.respondText(status = HttpStatusCode.NotFound) { "There is no record with id: $id" }
    }
  // create a person
  post("/people) {
    val person = call.receive<Person>()
    val result = personRepository.save(person.copy(id = UUID.randomUUID()))
    call.respond(result)
  }
}
Enter fullscreen mode Exit fullscreen mode

Before you move onto the next section, I want to show you how I actually implemented the routing:

fun Application.module() {
  val personRepository by kodein.instance<PersonRepository>()
  // route requests to handler functions
  routing { people(personRepository) }
}

// extracted to a separate extension function to tidy up the code
fun Routing.people(personRepository: PersonRepository) {
  route("/people") {
    // get a person
    get("/{id}") {
      val id = UUID.fromString(call.parameters["id"]!!)
      personRepository.find(id)?.let {
        call.respond(HttpStatusCode.OK, it)
      } ?: call.respondText(status = HttpStatusCode.NotFound) { "There is no record with id: $id" }
    }
    // create a person
    post {
      val person = call.receive<Person>()
      val result = personRepository.save(person.copy(id = UUID.randomUUID()))
      call.respond(result)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

I extracted the code to a separate function to decrease the contents of Application.module. This is going to be a good idea when you are trying to write a more significant application. Whether how I went about it is the Ktor way or not, is another question. From having a quick look at the Ktor docs, it looks like this is a decent solution. I believe I saw another way to do this, but I would need to spend more time with it.

Contents of a request handler

The code that executes when a request is routed to a request handler is obviously pretty important. The function needs to do something after all…

Each handler function executes within the context of a coroutine. I did not really make any use of this fact since each of the functions I have shown are fully synchronous. For a bit more information on this, the Ktor docs have an async example.

For the remainder of this post, I am going to try and not mention coroutines too much since they are not particularly important for this simple REST API.

In this section, the get function will be examined a little closer:

get("/{id}") {
  val id = UUID.fromString(call.parameters["id"]!!)
  personRepository.find(id)?.let {
    call.respond(HttpStatusCode.OK, it)
  } ?: call.respondText(status = HttpStatusCode.NotFound) { "There is no record with id: $id" }
}
Enter fullscreen mode Exit fullscreen mode

{id} indicates that a path variable is expected in the request and its value will be stored as id. Multiple path variables can be included, but only one is needed for this example 👍. The value of id is retrieved from call.parameters which takes in the name of the variable you want to access.

  • call represents the context of the current request.
  • parameters is a list of the request’s parameters.

Using the id from the path variables, the database searches for the corresponding record. In this scenario, if it exists, the record is returned along with the appropriate 200 OK. If it doesn’t, an error response is returned. Both respond and respondText alter the underlying response of the current call. You could do this manually, for example, by using:

call.response.status(HttpStatusCode.OK)
call.response.pipeline.execute(call, it)
Enter fullscreen mode Exit fullscreen mode

You could do that, but there isn’t any need to since that is actually just the implementation of respond. respondText has some extra logic but delegates down to response to finalise everything. The final call to execute in this function represents the return value of the function.

Installing extra features

In Ktor, extra features can be plugged in when needed. For example, Jackson JSON parsing can be added to handle and return JSON from your application. Below are the features installed to the example application:

fun Application.module() {
  install(DefaultHeaders) { header(HttpHeaders.Server, "My ktor server") }
  // controls what level the call logging is logged to
  install(CallLogging) { level = Level.INFO }
  // setup jackson json serialisation
  install(ContentNegotiation) { jackson() }
}
Enter fullscreen mode Exit fullscreen mode
  • DefaultHeaders adds a header to every response with the name of the server.

  • CallLogging logs information about outgoing responses and specifies what level to log them at. A logging library needs to be included for this to work. The output will look something like:

INFO ktor.application.log - 200 OK: GET - /people/302a1a73-173b-491c-b306-4d95387a8e36
Enter fullscreen mode Exit fullscreen mode
  • ContentNegotiation tells the server to use Jackson for incoming and outbound requests. Remember this required including ktor-jackson as a dependency. You could also use GSON if you prefer.

For a long list of the other features that Ktor includes, here is a handy link to their docs.

Installing features ties all the way back to the routing done earlier. routing delegates down to install inside its implementation. So you could write:

install(Routing) {
  route("/people") {
    get {
      // implementation
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Whatever floats your boat, but I’d just stick to using routing. Hopefully that helped you understand what is going on under the hood, even if it was just a little bit.

Brief mention for Kodein

I want to have a very brief look at Kodein since I used it in this post. Kodein is a dependency injection framework written in Kotlin, for Kotlin. Below is the super small amount of DI that I used for the example application:

val kodein = Kodein {
  bind<CqlSession>() with singleton { cassandraSession() }
  bind<PersonRepository>() with singleton { PersonRepository(instance()) }
}
val personRepository by kodein.instance<PersonRepository>()
Enter fullscreen mode Exit fullscreen mode

Inside of the Kodein block, instances of the application’s classes are created. In this scenario, only one instance of each class is needed. Calling singleton denotes this. instance is a placeholder provided by Kodein to pass into a constructor instead of the actual object.

Outside of the Kodein block, an instance of PersonRespository is retrieved.

Yeah, I know, there isn’t really much point to the use of Kodein here since I could have replaced it with a single line…

val personRepository = PersonRepository(cassandraSession())
Enter fullscreen mode Exit fullscreen mode

Instead, let’s think about it as a very concise example to understand 👍.

Closing thoughts

As someone that is extremely biased towards Spring, I found working with Ktor very different from what I am used to. It took me a bit longer than usual to work towards some sample code that I was happy with. That being said, I think the outcome looks ok, and I will need to spend some more time with Ktor to better understand precisely how to get the best out of it. At the moment, I am confident there is a lot more to squeeze out of Ktor. For more information on Ktor, I’ll have to refer you again to their documentation where they have plenty of samples and tutorials.

If you found this post helpful, you can follow me on Twitter at @LankyDanDev to keep up with my new posts.

Top comments (4)

Collapse
 
jillesvangurp profile image
Jilles van Gurp

I agree ktor looks interesting. I'd be interested in using the native version of that when the native compiler comes out of beta. IMHO that makes perfect sense for small microservices that don't need a lot of cpu and memory.

BTW. Spring is going to be supporting co-routines from 5.2 and with stuff like Flow, a lot of the reactive stuff they currently push is going to look pretty dated. IMHO Spring is very good facilitating Kotlin developers but you do end up with a lot of legacy complexity. Ktor does not have that problem yet.

Compared to Spring, Kodein is pretty verbose. What I like with Spring is that simply slapping a few @Component annotations in the right places causes Spring to do it's thing. Mostly explicitly instantiating things or autowiring them is done implicitly these days. The constructor for Bar expects a Foo instance and there's a Foo class with @Component means that spring creates and injects the instance for you without needing to be told to.

Kodein is a step up from DYI dependency injection of course but not a big one since it requires more ceremony. Spring uses a reflection which is not available on native or the js platform to automate the bits that you have to spell out with Kodein.

Collapse
 
rjaros profile image
Robert Jaros

You can easily use Ktor with Guice. This way creating and injecting components is just as easy as with Spring.

Collapse
 
jillesvangurp profile image
Jilles van Gurp

Like Spring this works only on the JVM. A multi platform injection framework would be needed long term. If, I'm on the JVM, I'd probably prefer spring for dependency injection since it is less verbose.

Collapse
 
theonlytails profile image
TheOnlyTails

Just so you know - Ktor adds an extesion function to String called uuid, that creates a new UUID for that string. No need to use UUID.fromString!