DEV Community

Cover image for πŸ… Http4k: Top 5 Server-Side Frameworks for Kotlin in 2022
Roger ViΓ±as Alcon
Roger ViΓ±as Alcon

Posted on • Edited on

πŸ… Http4k: 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 Http4k implementation, you can check the other ones in this series too.

To begin with you can follow the Quickstart and How-to guides.

To create a Http4k project we have three alternatives:

For example this project has been created using Project Wizard and these options:

  • What kind of app are you writing? Server
  • Do you need server-side WebSockets or SSE? No
  • Select a server engine: Undertow (http4k team's default choice)
  • Select HTTP client library: OkHttp (http4k team's go-to HTTP Client)
  • Select JSON serialisation library: Jackson (http4k team's pick for JSON library, although we will not need it for this sample)
  • Select a templating library: None
  • Select any other messaging formats used by the app: None (not needed for this sample but good to know)
  • Select any integrations that catch your eye! None (not needed for this sample but good to know)
  • Select any testing technologies to battle harden the app: None (not needed for this sample but good to know)
  • Application identity:
    • Main class name: GreetingApplication
    • Base package name: org.rogervinas
  • Select a build tool: Gradle
  • Select a packaging type: ShadowJar

Note some features missing with Project Wizard:

  • It generates Gradle Groovy (we cannot choose Gradle Kotlin DSL 😭)
  • It only configures Java 11
  • It does not generate a .gitignore file

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 endpoints:



curl http://localhost:9000/ping
pong

curl http://localhost:9000/formats/json/jackson
{"subject":"Barry","message":"Hello there!"}


Enter fullscreen mode Exit fullscreen mode

Implementation

YAML configuration

Http4k does not support loading configuration values from YAML 😨

For this sample we will just enable Cloud Native Configuration and get configuration values from environment variables using the Environment object.

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 Http4k 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)
  • As we plan to use Postgres we need to add org.postgresql:postgresql dependency in build.gradle.

GreetingController

We create a GreetingController to serve the /hello endpoint:



fun greetingController(
  name: String,
  secret: String,
  repository: GreetingRepository
) = routes(
  "/hello" bind GET to {
    Response(Status.OK)
      .body("${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.

Vault configuration

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



private fun Environment.withVault(): Environment {
  val vaultProtocol = EnvironmentKey.string().defaulted(VAULT_PROTOCOL, "http")(this)
  val vaultHost = EnvironmentKey.string().defaulted(VAULT_HOST, "localhost")(this)
  val vaultPort = EnvironmentKey.int().defaulted(VAULT_PORT, 8200)(this)
  val vaultToken = EnvironmentKey.string().defaulted(VAULT_TOKEN, "mytoken")(this)
  val vaultPath = EnvironmentKey.string().defaulted(VAULT_PATH, "secret/myapp")(this)
  val vaultConfig = VaultConfig()
    .address("$vaultProtocol://$vaultHost:$vaultPort")
    .token(vaultToken)
    .build()
  val vaultData = Vault(vaultConfig).logical().read(vaultPath).data
  return MapEnvironment.from(vaultData.toProperties()).overrides(this)
}


Enter fullscreen mode Exit fullscreen mode

We use Environment from Cloud Native Configuration to retrieve environment variables and override them with all values loaded from Vault.

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

Application

There is no particular convention in Http4k so we will make our own:

We will create a GreetingRepository:



private fun greetingRepository(env: Environment): GreetingRepository {
  val host = EnvironmentKey.string().defaulted(DB_HOST, "localhost")(env)
  val port = EnvironmentKey.int().defaulted(DB_PORT, 5432)(env)
  val name = EnvironmentKey.string().defaulted(DB_NAME, "mydb")(env)
  val username = EnvironmentKey.string().defaulted(DB_USERNAME, "myuser")(env)
  val password = EnvironmentKey.string().defaulted(DB_PASSWORD, "mypassword")(env)
  val connection = DriverManager.getConnection("jdbc:postgresql://$host:$port/$name", username, password)
  return GreetingJdbcRepository(connection)
}


Enter fullscreen mode Exit fullscreen mode

And a HttpHandler (our GreetingController):



private fun greetingController(env: Environment, repository: GreetingRepository): HttpHandler {
  val name = EnvironmentKey.string().defaulted(GREETING_NAME, "Bitelchus")(env)
  val secret = EnvironmentKey.string().defaulted(GREETING_SECRET, "unknown")(env)
  return greetingController(name, secret, repository)
}


Enter fullscreen mode Exit fullscreen mode

And a Http4kServer (our GreetingApplication):



fun greetingApplication(env: Environment): Http4kServer {
  val envWithVault = env.withVault()
  val port = EnvironmentKey.int().defaulted(SERVER_PORT, 8080)(envWithVault)
  val repository = greetingRepository(envWithVault)
  val controller = greetingController(envWithVault, repository)
  return PrintRequest().then(controller).asServer(Undertow(port))
}


Enter fullscreen mode Exit fullscreen mode

And finally we will start it in the main method:



fun main() {
  println("Application starting ...")
  val application = greetingApplication(ENV)
  application.start()
  println("Application started on " + application.port())
}


Enter fullscreen mode Exit fullscreen mode

Note that in greetingApplication function we override the ENV object with all the values loaded from Vault.

Testing the endpoint

We can test the endpoint this way:



class GreetingControllerTest {

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

  private val controller = greetingController("Bitelchus", "apple", repository)

  @Test
  fun `should say hello`() {
    val response = controller(Request(GET, "/hello"))
    assertThat(
      response,
      hasStatus(Status.OK)
    )
    assertThat(
      response, 
      hasBody("Hello my name is Bitelchus and my secret is apple")
    )
  }
}


Enter fullscreen mode Exit fullscreen mode

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))
      .waitingFor("vault-cli", forLogMessage(".*created_time.*", 1))
  }

  private val client = OkHttp()
  private val application = greetingApplication(
    MapEnvironment.from(
      Properties().apply {
        this[SERVER_PORT] = 0
      }
    )
  )

  @BeforeEach
  fun start() {
    application.start()
  }

  @AfterEach
  fun stop() {
    application.stop()
  }

  @Test
  fun `should say hello`() {
    val response = client(Request(GET, "http://localhost:${application.port()}/hello"))
    assertThat(
      response, 
      hasStatus(OK)
    )
    assertThat(
      response,
      hasBody(Regex(".+ 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 can override configuration values passing a MapEnvironment to the greetingApplication. In this case we only override SERVER_PORT value to 0 to force a random port.
  • We use pattern matching to check the greeting, as it is random.
  • As this test uses Vault, the secret should be watermelon.

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:



mainClassName = "org.rogervinas.GreetingApplicationKt"


Enter fullscreen mode Exit fullscreen mode

Build a fatjar and run it



# Build fatjar
./gradlew shadowJar

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

# Start Application
java -jar build/libs/http4k-app.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

Build a docker image and run it

Http4k does not support creating docker images out of the box, but for example we can create this Dockerfile:



FROM registry.access.redhat.com/ubi8/openjdk-11:1.15

COPY build/libs/http4k-app.jar /app/

EXPOSE 8080

ENV JAVA_APP_JAR="/app/http4k-app.jar"


Enter fullscreen mode Exit fullscreen mode

And then:



# Build fatjar
./gradlew shadowJar

# Build docker image and publish it to local registry
docker build . -t http4k-app

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

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

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

# Stop all containers
docker compose --profile http4k down
docker compose down


Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)