Skip to main content
Version: 4.2

Ktor & Annotations

In this tutorial you build a Ktor web app with a /hello endpoint that greets a user picked from a list. You learn how to annotate classes, scan them into a module, and load them in the Koin Ktor plugin from a @KoinApplication object. It takes about 10 minutes and needs a JDK 17 and Gradle.

This tutorial uses annotations. For the same app with the Compiler Plugin DSL, see Ktor. To compare the styles, see Choosing your style.

Get the code​

The full sample is in koin-getting-started/ktor-annotations. Clone the repository and run the app module from the ktor-annotations folder.

Gradle setup​

The sample applies the Koin Compiler Plugin and adds koin-ktor, koin-annotations and koin-logger-slf4j. Versions live in a version catalog:

# gradle/libs.versions.toml
[versions]
# Kotlin, Ktor and Logback entries omitted
koin = "<KOIN_VERSION>"
koin-plugin = "<KOIN_PLUGIN_VERSION>"

[libraries]
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin" }
koin-logger-slf4j = { module = "io.insert-koin:koin-logger-slf4j", version.ref = "koin" }
koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" }
koin-test-junit4 = { module = "io.insert-koin:koin-test-junit4", version.ref = "koin" }

[plugins]
koin-compiler = { id = "io.insert-koin.compiler.plugin", version.ref = "koin-plugin" }
// app/build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.koin.compiler)
application
}

dependencies {
implementation(libs.ktor.server)
implementation(libs.logback)
implementation(libs.koin.ktor)
implementation(libs.koin.annotations)
implementation(libs.koin.logger.slf4j)

testImplementation(libs.ktor.test)
testImplementation(libs.koin.test)
testImplementation(libs.koin.test.junit4)
}

The Compiler Plugin processes the annotations and needs Kotlin 2.3.20 or later. koin-annotations shares the Koin version. For details, see Gradle setup and Compiler Plugin setup.

The application​

The /hello route asks UserService for a user, and UserService reads from UserRepository:

User -> UserRepository -> UserService -> /hello route

Let's write the user data​

Start with the data class and a default list of users:

data class User(val name: String, val email: String)

val DEFAULT_USER = User("Alice", "alice@example.com")

val DEFAULT_USERS = listOf(
DEFAULT_USER,
User("Bob", "bob@example.com"),
User("Charlie", "charlie@example.com")
)

These users are the data that the repository stores.

Let's write the repository​

The repository keeps users in memory. Annotate the implementation with @Singleton:

import org.koin.core.annotation.Singleton

interface UserRepository {
fun findUserOrNull(name: String): User?
fun addUsers(users: List<User>)
}

@Singleton
class UserRepositoryImpl : UserRepository {

private val _users = arrayListOf<User>()

override fun findUserOrNull(name: String): User? {
return _users.firstOrNull { it.name == name }
}

override fun addUsers(users: List<User>) {
_users.addAll(users)
}
}

Koin binds UserRepositoryImpl to its supertypes, so a request for UserRepository gets this instance. @Singleton comes from org.koin.core.annotation and is equivalent to @Single (see Definitions).

Let's write the service​

UserService loads the users and prepares the greeting. It is a @Singleton too:

interface UserService {
fun getUserOrNull(name: String = DEFAULT_USER.name): User?
fun loadUsers()
fun prepareHelloMessage(user: User?): String
}

@Singleton
class UserServiceImpl(
private val userRepository: UserRepository
) : UserService {

override fun getUserOrNull(name: String): User? = userRepository.findUserOrNull(name)

override fun loadUsers() {
userRepository.addUsers(DEFAULT_USERS)
}

override fun prepareHelloMessage(user: User?): String {
return user?.let { "Hello '${user.name}' (${user.email})!" } ?: "User not found"
}
}

The Compiler Plugin reads the constructor and passes the UserRepository instance for you.

Let's declare the Koin module​

A module groups definitions (see Core Concepts). Declare a module class that scans the sample package:

import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Configuration
import org.koin.core.annotation.Module

@Module
@ComponentScan("org.koin.sample")
@Configuration
class AppModule

@ComponentScan registers every annotated class in org.koin.sample and its subpackages. @Configuration makes the module discoverable by a @KoinApplication entry point. Both are described in Modules.

Let's install Koin and write the route​

Declare a @KoinApplication object. Install the Koin plugin and load the configuration with withConfiguration<T>(), then inject UserService in the /hello route:

import org.koin.core.annotation.KoinApplication
import org.koin.ktor.ext.inject
import org.koin.ktor.plugin.Koin
import org.koin.logger.slf4jLogger
import org.koin.plugin.module.dsl.withConfiguration

@KoinApplication
object KoinUserApplication

fun Application.main() {
install(Koin) {
slf4jLogger()
withConfiguration<KoinUserApplication>()
}

val service by inject<UserService>()
service.loadUsers()

routing {
get("/hello") {
val userName = call.queryParameters["name"] ?: "Alice"
val user = service.getUserOrNull(userName)
val message = service.prepareHelloMessage(user)
call.respondText(message)
}
}
}

withConfiguration<KoinUserApplication>() loads every @Configuration module, so you don't call modules(), and the plugin validates the full graph at build time. slf4jLogger() sends Koin logs to SLF4J. See Koin for Ktor for the Ktor-specific APIs.

Let's start the server​

Start an embedded Netty server that runs your application module:

fun main(args: Array<String>) {
embeddedServer(Netty, port = 8080) {
main()
}.start(wait = true)
}

Run the app, then open these URLs:

  • http://localhost:8080/hello: greets Alice, the default user.
  • http://localhost:8080/hello?name=Bob: greets Bob.

Next steps​