DEV Community

Cover image for ๐Ÿ… Ktor: Top 5 Server-Side Frameworks for Kotlin in 2022
Roger Viรฑas Alcon
Roger Viรฑas Alcon

Posted on • Edited on

๐Ÿ… Ktor: Top 5 Server-Side Frameworks for Kotlin in 2022

This is a demo inspired by @antonarhipov's Top 5 Server-Side Frameworks for Kotlin in 2022 @ Kotlin by JetBrains where, spoiler alert, the author shares this top 5 list:

๐Ÿฅ‡ Spring Boot
๐Ÿฅˆ Quarkus
๐Ÿฅ‰ Micronaut
๐Ÿ… Ktor
๐Ÿ… Http4k

I have a lot of experience in Spring Boot, so I wanted to take a look at the other ones ๐Ÿ˜œ
Meme

To do so we will create a simple application with each one of these frameworks, implementing the following scenario:
Scenario

GitHub logo rogervinas / top-5-server-side-kotlin-frameworks-2022

โญ Top 5 Server-Side Frameworks for Kotlin in 2022

This post will describe the step-by-step Ktor implementation, you can check the other ones in this series too.

To begin with you can follow the Creating Ktor applications guide.

To create a Ktor project we have three alternatives:

For example this project has been created using start.ktor.io and these options:

  • Adjust project settings:
    • Build system = Gradle Kotlin
    • Engine = Netty
    • Configuration in = YAML file
    • Add sample code โœ“
  • Add plugins:
    • Postgres โœ“

Once created you can run it to check everything is ok:

./gradlew run
Enter fullscreen mode Exit fullscreen mode

And make a request to the sample endpoint:

curl http://localhost:8080
Hello World!
Enter fullscreen mode Exit fullscreen mode

Implementation

YAML configuration

As we generated the project choosing "Configuration in YAML file" all is set, and we can add our custom property in application.yaml:

greeting:
  name: "Bitelchus"
Enter fullscreen mode Exit fullscreen mode

If we had chosen "Configuration in Code" we would need to make these changes:

1) Add io.ktor:ktor-server-config-yaml dependency.
2) Change Application's main method from:

  fun main() {
    embeddedServer(
      factory = Netty,
      port = 8080,
      host = "0.0.0.0",
      module = Application::module
    )
      .start(wait = true)
  }
Enter fullscreen mode Exit fullscreen mode

To:

  fun main() {
    EngineMain.main(args)
  }
Enter fullscreen mode Exit fullscreen mode

3) Add Application's port and modules in application.yaml:

ktor:
  deployment:
    port: 8080
  application:
    modules:
      - org.rogervinas.GreetingApplicationKt.module
Enter fullscreen mode Exit fullscreen mode

GreetingRepository

We will create a GreetingRepository:

interface GreetingRepository {
  fun getGreeting(): String
}

class GreetingJdbcRepository(private val connection: Connection) : GreetingRepository {

  init {
    createGreetingsTable() 
  }

  override fun getGreeting(): String = connection
    .createStatement()
    .use { statement ->
      statement
        .executeQuery("""
          SELECT greeting FROM greetings
          ORDER BY random() LIMIT 1
          """.trimIndent()
        )
        .use { resultSet ->
          return if (resultSet.next()) {
            resultSet.getString("greeting")
          } else {
            throw Exception("No greetings found!")
          }
        }
  }

  private fun createGreetingsTable() {
   connection.createStatement().use {
     it.executeUpdate("""
       CREATE TABLE IF NOT EXISTS greetings (
         id serial,
         greeting varchar(100) NOT NULL,
         PRIMARY KEY (id)
       );
       INSERT INTO greetings (greeting) VALUES ('Hello');
       INSERT INTO greetings (greeting) VALUES ('Hola');
       INSERT INTO greetings (greeting) VALUES ('Hi');
       INSERT INTO greetings (greeting) VALUES ('Holi');
       INSERT INTO greetings (greeting) VALUES ('Bonjour');
       INSERT INTO greetings (greeting) VALUES ('Ni hao');
       INSERT INTO greetings (greeting) VALUES ('Bon dia');
       """.trimIndent()
     )
   }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • As Ktor does not offer any specific database support:
    • We just use plain java.sql code (instead of any database connection library)
    • We just create the greetings table if it does not exist (instead of any database migration library like flyway)
  • Adding Postgres plugin when creating the project should add two dependencies:
    • org.postgresql:postgresql for production.
    • com.h2database:h2 for testing, that we can remove it as we will use Testcontainers

We create this function to create the repository:

private fun greetingRepository(config: ApplicationConfig): GreetingRepository {
  val host = config.property("database.host").getString()
  val port = config.property("database.port").getString()
  val name = config.property("database.name").getString()
  val username = config.property("database.username").getString()
  val password = config.property("database.password").getString()
  val connection = DriverManager.getConnection("jdbc:postgresql://$host:$port/$name", username, password)
  return GreetingJdbcRepository(connection)
}
Enter fullscreen mode Exit fullscreen mode

And we add these properties in application.yaml:

database:
  host: "$DB_HOST:localhost"
  port: 5432
  name: "mydb"
  username: "myuser"
  password: "mypassword"
Enter fullscreen mode Exit fullscreen mode

Note that we allow to override database.host with the value of DB_HOST environment variable, or "localhost" if not set. This is only needed when running locally using docker compose.

GreetingController

We create a GreetingController to serve the /hello endpoint:

fun Application.greetingController(
  name: String,
  secret: String,
  repository: GreetingRepository
) {
  routing {
    get("/hello") {
      call.respondText {
        "${repository.getGreeting()} my name is $name and my secret is $secret"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

We just name it GreetingController to follow the same convention as the other frameworks in this series, mainly SpringBoot.

Complete documentation at Routing guide.

Vault configuration

Ktor does not support Vault, so we will simply use BetterCloud/vault-java-driver:

private fun ApplicationConfig.withVault(): ApplicationConfig {
  val vaultProtocol = this.property("vault.protocol").getString()
  val vaultHost = this.property("vault.host").getString()
  val vaultPort = this.property("vault.port").getString()
  val vaultToken = this.property("vault.token").getString()
  val vaultPath = this.property("vault.path").getString()
  val vaultConfig = VaultConfig()
    .address("$vaultProtocol://$vaultHost:$vaultPort")
    .token(vaultToken)
    .build()
  val vaultData = Vault(vaultConfig).logical().read(vaultPath).data
  return this.mergeWith(MapApplicationConfig(vaultData.entries.map { Pair(it.key, it.value) }))
}
Enter fullscreen mode Exit fullscreen mode

With these properties in application.yaml:

vault:
  protocol: "http"
  host: "$VAULT_HOST:localhost"
  port: 8200
  token: "mytoken"
  path: "secret/myapp"
Enter fullscreen mode Exit fullscreen mode

Note that we allow to override vault.host with the value of VAULT_HOST environment variable, or "localhost" if not set. This is only needed when running locally using docker compose, same as with database.host.

As an alternative, we could also use karlazzampersonal/ktor-vault plugin.

Next section will show how to use this ApplicationConfig.withVault() extension.

Application

As defined in application.yaml the only module loaded will be org.rogervinas.GreetingApplicationKt.module so we need to implement it:

fun Application.module() {
  val environmentConfig = environment.config.withVault()
  val repository = greetingRepository(environmentConfig)
  greetingController(
    environmentConfig.property("greeting.name").getString(),
    environmentConfig.propertyOrNull("greeting.secret")?.getString() ?: "unknown",
    repository
  )
}
Enter fullscreen mode Exit fullscreen mode
  • It will merge default ApplicationConfig and override it with Vault values.
  • It will create a GreetingRepository and a GreetingController.

Testing the endpoint

We can test the endpoint this way:

class GreetingControllerTest {

  private val repository = mockk<GreetingRepository>().apply {
    every { getGreeting() } returns "Hello"
  }

  @Test
  fun `should say hello`() = testApplication {
    environment {
      config = MapApplicationConfig()
     }
     application {
       greetingController("Bitelchus", "apple", repository)
     }
     client.get("/hello").apply {
       assertThat(status).isEqualTo(OK)
       assertThat(bodyAsText())
        .isEqualTo("Hello my name is Bitelchus and my secret is apple")
     }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • We use testApplication DSL with an empty configuration to just test the controller.
  • We mock the repository with mockk.
  • Complete documentation at Testing guide.

Testing the application

We can test the whole application this way:

@Testcontainers
class GreetingApplicationTest {

  companion object {
    @Container
    private val container = DockerComposeContainer(File("../docker-compose.yaml"))
      .withServices("db", "vault", "vault-cli")
      .withLocalCompose(true)
      .waitingFor("db", forLogMessage(".*database system is ready to accept connections.*", 1))
      .waitingFor("vault", forLogMessage(".*Development mode.*", 1))
  }

  @Test
  fun `should say hello`() = testApplication {
    client.get("/hello").apply {
      assertThat(status).isEqualTo(OK)
      assertThat(bodyAsText())
        .matches(".+ my name is Bitelchus and my secret is watermelon")
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • We use Testcontainers to test with Postgres and Vault containers.
  • We use pattern matching to check the greeting, as it is random.
  • As this test uses Vault, the secret should be watermelon.
  • Complete documentation at Testing guide.

Test

./gradlew test
Enter fullscreen mode Exit fullscreen mode

Run

# Start Vault and Database
docker compose up -d vault vault-cli db

# Start Application
./gradlew run

# Make requests
curl http://localhost:8080/hello

# Stop Application with control-c

# Stop all containers
docker compose down
Enter fullscreen mode Exit fullscreen mode

Note that main class is specified in build.gradle.kts:

application {
  mainClass.set("org.rogervinas.GreetingApplicationKt")
}
Enter fullscreen mode Exit fullscreen mode

Build a fatjar and run it

# Build fatjar
./gradlew buildFatJar

# Start Vault and Database
docker compose up -d vault vault-cli db

# Start Application
java -jar build/libs/ktor-app-all.jar

# Make requests
curl http://localhost:8080/hello

# Stop Application with control-c

# Stop all containers
docker compose down
Enter fullscreen mode Exit fullscreen mode

More documentation at Creating fat JARs guide.

Build a docker image and run it

# Build docker image and publish it to local registry
./gradlew publishImageToLocalRegistry

# Start Vault and Database
docker compose up -d vault vault-cli db

# Start Application
docker compose --profile ktor up -d

# Make requests
curl http://localhost:8080/hello

# Stop all containers
docker compose --profile ktor down
docker compose down
Enter fullscreen mode Exit fullscreen mode

We can configure "Image name", "Image tag" and "JRE version" in build.gradle.kts:

ktor {
  docker {
    localImageName.set("${project.name}")
    imageTag.set("${project.version}")
    jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_17)
  }
}
Enter fullscreen mode Exit fullscreen mode

More documentation at Docker guide.

That's it! Happy coding! ๐Ÿ’™

Top comments (0)