Kotlin web service using Ktor, Guice and Jackson

In this post, I will tell you how to set up a simple web stack for the HTTP REST API in Kotlin using Ktor as a web framework, Guice for the dependency injection and Jackson for the JSON serialization.

Initially, when I decided to write a web service using Kotlin programming language I had to make a research and I decided to go with the Ktor (ktor.io) as a web framework. If you’re new to this framework I’d recommend you to check the documentation. Here is an entry point for the basic application creation flow: https://ktor.io/docs/a-ktor-application.html

It’s really easy to start writing simple web services using Ktor. The framework itself is flexible and allows to configure a lot of things out of the box.

However, after some time your app becomes more complex and it’s important to separate different components to keep the app maintainable and reusable.

The common way to do that is to inject dependencies to the places where they needed. This provides inversion of control and removes the need for the services to know the way how to create other services.

Instead of manually injection dependencies, I like to use a lightweight dependency injection framework called Guice (https://github.com/google/guice/) and I’m going to use it in this project.

I don’t want to go too much into details how to use Guice and I recommend to read docs (https://github.com/google/guice/wiki/GettingStarted) if you’re not familiar with it, but usually, the workflow looks like this:

  • Declare service classes and annotate them with @Inject annotation:
class AnotherService
class MyService @Inject constructor(
private val service: AnotherService) {
fun start() {
//Start your service here
}
}
  • Create a Guice module:
class MyModule : AbstractModule() {

override fun configure() {
bind(AnotherService::class.java).asEagerSingleton()
bind(MyService::class.java).asEagerSingleton()

}
}
  • Create an injector and start services:
val modules = listOf(
MyModule(),
// Other modules here
)

val injector = Guice.createInjector(modules)
val service = injector.getInstance(MyService::class.java)
service.start()

At this point Guice will resolve all the required dependencies, create and link them, so you can get your service from the injector and start it.

The simple Ktor application looks like this:

fun main() {
embeddedServer(Netty, port = 8000) {
routing {
get("/") {
call.respondText("Hello, world!")
}
}
}
.start(wait = true)
}

You configure a server, a routing and then you can start the server.

However, if you’d like to have multiple routes in the routing it becomes to looks really big and messy. Ktor suggests to use splitting the routing by features (https://ktor.io/docs/structuring-applications.html#grouping-by-features) however this is still isn’t good enough if you’d like to use some services from your routes (which you likely would).

First of all to solve this problem let’s introduce an interface to make it possible to create subclasses of this interface for different routes and allow them to keep some state:

interface Routes {

fun config(): Route.() -> Unit
}

Then we can create our class which will be sending a greeting to the person who called the API:

class TestRoutes : Routes {

override fun config(): Route.() -> Unit = fun Route.() {
get("greet") {
call.respond(mapOf("message" to "Hello world!"))
}
}
}

We also need to configure routes in the routing of the Ktor App, for that we need to pass the instance of Route to our new created TestRoutes class:

routing {
TestRoutes().config().invoke(this)
}

But imagine that we would like to create a greeting service which will return a different greeting based on a day of the week or other parameters:

class GreetingProvider {
fun getGreeting(): String {
return "Hello World!"
}
}
fun main() {
val greetingProvider = GreetingProvider()
val testRoutes = TestRoutes(greetingProvider)
embeddedServer(Netty, port = 8000) {
routing {
testRoutes.config().invoke(this)
}
}
.start(wait = true)
}

So it’s already starting to look a bit overwhelming and messy, you have to manually create different classes, link them together and configure the server. Let’s see how can we do it better.

First of all, we need to have a way to inject all the routes, other dependencies, and the rest of the configuration to the ApplicationEngine instance which is created embeddedServer function.

We can’t bind and inject configuration to the ApplicationEngine straightforwardly because both embeddedServer or NettyApplicationEngine constructor should be configured with either environment or configuration function.

To deal with this problem we can use Provider class from Guice (https://github.com/google/guice/wiki/ProviderBindings).

The idea here that we will create a provider class which will call embeddedServer and create ApplicationEngine for us when we need it. Basically, this is the replacement of the @Inject annotation for the classes we have no control over. You can inject all the dependencies to the Provider class and then pass them to your factory method:

class ApplicationEngineProvider @Inject constructor(
private val routes: java.util.Set<Routes>
) : Provider<ApplicationEngine> {

override fun get(): ApplicationEngine {
return embeddedServer(Netty) {
routing {
for (route in routes) {
route.config().invoke(this)
}
}
}
}
}

Also we have to bind this provider to our class in the module like this:

class ApplicationModule : AbstractModule() {
override fun configure() {
install(RouteModule()) // This is where we bind routes, I describe it later.
bind(ApplicationEngine::class.java).toProvider(ApplicationEngineProvider::class.java).asEagerSingleton()
}
}

Now you can start your service:

fun main() {
val modules = listOf(ApplicationModule())

val injector = Guice.createInjector(modules)
val engine = injector.getInstance(ApplicationEngine::class.java)

engine.start(wait = true)
}

So far I’ve skipped the part where we configure all the routes and pass them to the engine, let’s get back to it.
We would love to have multiple routes to logically separate our code, but then we don’t need to need to distinguish between them while we creating ApplicationEngine. For that Guice provides a way to bind a bunch of entities sharing the same interface to a single collection. You have to create a Multibinder and then bind all of your entities with it:

class RouteModule : AbstractModule() {

override fun configure() {
val routeBinder = Multibinder.newSetBinder(binder(), Routes::class.java)
routeBinder.addBinding().to(TestRoutes::class.java)
routeBinder.addBinding().to(OtherRoutes::class.java)
}
}

Then you can inject all the routes to the provider as I showed above in the ApplicationEngineProvider example.

Eventually, let’s see how do we use dependencies in the routes. We would like to inject GreetingProvider to the TestRoutes to use it when we need to return a greeting to the client:

class TestRoutes @Inject constructor(private val greetingProvider: GreetingProvider) : Routes {

override fun config(): Route.() -> Unit = fun Route.() {
get("greet") {
call.respond(mapOf("message" to greetingProvider.getGreeting()))
}
}
}

We will also need to bind GreetingProvider in one of the modules

bind(GreetingProvider::class.java).asEagerSingleton()

After that we should be able to start the app and Guice will bootstrap the service and link all the dependencies.

Another example is to configure Jackson ObjecMapper to serialize model to the JSON in Ktor.

By default you can install content negotiation module to Ktor like this:

install(ContentNegotiation) {
jackson { }
}

However, this code will create a new ObjectMapper, if you would like to share the same object mapper in different parts of your system you should configure ObjectMapper and inject it to the Provider:

class JsonModule : AbstractModule() {

@Provides
@Named("api")
fun apiObjectMapper(): ObjectMapper {
return ObjectMapper()
.registerKotlinModule()
.enable(SerializationFeature.INDENT_OUTPUT)
}
}

Then you can update ApplicationEngineProvider to look like that:

class ApplicationEngineProvider @Inject constructor(
private val objectMapper: ObjectMapper,
private val routes: java.util.Set<Routes>
) : Provider<ApplicationEngine> {

override fun get(): ApplicationEngine {
return embeddedServer(Netty) {
install(ContentNegotiation) {
register(
ContentType.Application.Json,
JacksonConverter(objectMapper)
)
}
routing {
for (route in routes) {
route.config().invoke(this)
}
}
}
}
}

After that, your application will serialize responses and deserialize an input body using preconfigured ObjectMapper.

This is the quick and easy way how you can make Ktor app be more modular and don’t worry about the injecting dependencies yourself.

An example project can be found on Github:

https://github.com/v1ctor/inject-dependencies-to-ktor-app-with-guice

Software Engineer, Facebook