Kotlin Web Project Examples

This page contains examples of using Mill as a build tool for web-applications. It covers setting up a basic backend server with a variety of server frameworks

Ktor Hello World App

build.mill (download, browse)
package build
import mill._, kotlinlib._

object `package` extends RootModule with KotlinModule {

  def kotlinVersion = "1.9.24"

  def mainClass = Some("com.example.HelloKtorKt")

  def ivyDeps = Agg(
    ivy"io.ktor:ktor-server-core-jvm:2.3.12",
    ivy"io.ktor:ktor-server-netty-jvm:2.3.12"
  )

  object test extends KotlinTests with TestModule.Junit5 {
    def ivyDeps = super.ivyDeps() ++ Agg(
      ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1",
      ivy"io.ktor:ktor-server-test-host-jvm:2.3.12"
    )
  }
}
src/com/example/HelloKtor.kt (browse)
package com.example

import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing

fun main() {
    embeddedServer(Netty, port = 8090, host = "0.0.0.0", module = Application::module)
        .start(wait = true)
}

fun Application.module() {
    routing {
        get("/") {
            call.respondText("<h1>Hello, World!</h1>")
        }
    }
}
test/src/com/example/HelloKtorTest.kt (browse)
package com.example

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import io.ktor.server.testing.testApplication

class HelloKtorTest :
    FunSpec({
        test("HelloKtorTest") {
            testApplication {
                application {
                    module()
                }
                val response = client.get("/")
                response.status shouldBe HttpStatusCode.OK
                response.bodyAsText() shouldBe "<h1>Hello, World!</h1>"
            }
        }
    })

This example demonstrates how to set up a simple webserver serving a single "<h1>Hello, World!</h1>" web page using Kotlin and Ktor. It includes one module which is the Ktor webserver/app, and one test module using kotest

> mill test
Test com.example.HelloKtorTest finished, took...

> mill runBackground

> curl http://localhost:8090
...<h1>Hello, World!</h1>...

> mill clean runBackground

Ktor TodoMvc App

This example implementing the well known TodoMVC example app using Kotlin and Ktor.

build.mill (download, browse)
package build
import mill._, kotlinlib._

object `package` extends RootModule with KotlinModule {

  def kotlinVersion = "1.9.24"

  def mainClass = Some("com.example.TodoMVCApplicationKt")

  val ktorVersion = "2.3.12"
  val exposedVersion = "0.53.0"

  def ivyDeps = Agg(
    ivy"io.ktor:ktor-server-core-jvm:$ktorVersion",
    ivy"io.ktor:ktor-server-netty-jvm:$ktorVersion",
    ivy"org.jetbrains.exposed:exposed-core:$exposedVersion",
    ivy"org.jetbrains.exposed:exposed-jdbc:$exposedVersion",
    ivy"com.h2database:h2:2.2.224",
    ivy"io.ktor:ktor-server-webjars-jvm:$ktorVersion",
    ivy"org.webjars:jquery:3.2.1",
    ivy"io.ktor:ktor-server-thymeleaf-jvm:$ktorVersion",
    ivy"org.webjars:webjars-locator:0.41",
    ivy"org.webjars.npm:todomvc-common:1.0.5",
    ivy"org.webjars.npm:todomvc-app-css:2.4.1",
    ivy"ch.qos.logback:logback-classic:1.4.14"
  )

  object test extends KotlinTests with TestModule.Junit5 {
    def ivyDeps = super.ivyDeps() ++ Agg(
      ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1",
      ivy"io.ktor:ktor-server-test-host-jvm:2.3.12"
    )
  }
}

Apart from running a webserver, this example also demonstrates:

  • Serving HTML templates using Thymeleaf

  • Serving static Javascript and CSS using Webjars

  • Querying a SQL database using Exposed

  • Testing using a H2 in-memory database

> mill test

> mill runBackground

> curl http://localhost:8091
...<h1>todos</h1>...

> mill clean runBackground

Simple KotlinJS Module

Kotlin/JS support on Mill is still Work In Progress (WIP). As of time of writing it supports Node.js, but lacks support of Browser, Webpack, test runners, etc.

The example below demonstrates only the minimal compilation, running, and testing of a single Kotlin/JS module using a single third-party dependency. For more details in fully developing Kotlin/JS support, see the following ticket:

build.mill (download, browse)
package build
import mill._, kotlinlib._, kotlinlib.js._

object `package` extends RootModule with KotlinJsModule {
  def moduleKind = ModuleKind.ESModule
  def kotlinVersion = "1.9.25"
  def kotlinJsRunTarget = Some(RunTarget.Node)
  def ivyDeps = Agg(
    ivy"org.jetbrains.kotlinx:kotlinx-html-js:0.11.0"
  )
  object test extends KotlinJsModule with KotestTests
}
> mill run
Compiling 1 Kotlin sources to .../out/compile.dest/classes...
<h1>Hello World</h1>
stringifiedJsObject: ["hello","world","!"]

> mill test # Test is incorrect, `test` fails
Compiling 1 Kotlin sources to .../out/test/compile.dest/classes...
Linking IR to .../out/test/linkBinary.dest/binaries
produce executable: .../out/test/linkBinary.dest/binaries
...
error: ... expected:<"<h1>Hello World Wrong</h1>"> but was:<"<h1>Hello World</h1>...
...

> cat out/test/linkBinary.dest/binaries/test.js # Generated javascript on disk
...shouldBe(..., '<h1>Hello World Wrong<\/h1>');...
...

> sed -i.bak 's/Hello World Wrong/Hello World/g' test/src/foo/HelloTests.kt

> mill test # passes after fixing test

Ktor Webapp, KotlinJS Client

A minimal example of a Kotlin backend server wired up with a Kotlin/JS front-end. The backend code is identical to the Ktor TodoMvc App example, but we replace the main.js client side code with the Javascript output of ClientApp.kt.

build.mill (download, browse)
package build

import mill._, kotlinlib._, kotlinlib.js._

object `package` extends RootModule with KotlinModule {

  def kotlinVersion = "1.9.24"
  def ktorVersion = "2.3.12"
  def kotlinHtmlVersion = "0.11.0"

  def mainClass = Some("webapp.WebApp")

  def ivyDeps = Agg(
    ivy"io.ktor:ktor-server-core-jvm:$ktorVersion",
    ivy"io.ktor:ktor-server-netty-jvm:$ktorVersion",
    ivy"io.ktor:ktor-server-html-builder-jvm:$ktorVersion"
  )

  def resources = Task {
    os.makeDir(Task.dest / "webapp")
    val jsPath = client.linkBinary().classes.path
    // Move root.js[.map]into the proper filesystem position
    // in the resource folder for the web server code to pick up
    os.copy(jsPath / "client.js", Task.dest / "webapp/client.js")
    os.copy(jsPath / "client.js.map", Task.dest / "webapp/client.js.map")
    super.resources() ++ Seq(PathRef(Task.dest))
  }

  object test extends KotlinTests with TestModule.Junit5 {
    def ivyDeps = super.ivyDeps() ++ Agg(
      ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1",
      ivy"io.ktor:ktor-server-test-host-jvm:$ktorVersion"
    )
  }

  object client extends KotlinJsModule {
    def kotlinVersion = "1.9.24"

    override def splitPerModule = false

    def ivyDeps = Agg(
      ivy"org.jetbrains.kotlinx:kotlinx-html-js:$kotlinHtmlVersion"
    )
  }
}

Note that the client-side Kotlin code is the simplest 1-to-1 translation of the original Javascript, using kotlinx.browser, as this example is intended to demonstrate the build.mill config in Mill. A real codebase is likely to use Javascript or Kotlin/JS UI frameworks to manage the UI, but those are beyond the scope of this example.

> ./mill test
...webapp.WebAppTestssimpleRequest ...

> ./mill runBackground

> curl http://localhost:8092
...What needs to be done...
...

> curl http://localhost:8092/static/client.js
...bindEvent(this, 'todo-all', '/list/all', 'all')...
...

> ./mill clean runBackground

Ktor KotlinJS Code Sharing

A Kotlin/JVM backend server wired up with a Kotlin/JS front-end, with a shared module containing code that is used in both client and server. Rather than the server sending HTML for the initial page load and HTML for page updates, it sends HTML for the initial load and JSON for page updates which is then rendered into HTML on the client.

build.mill (download, browse)
package build
import mill._, kotlinlib._, kotlinlib.js._

trait AppKotlinModule extends KotlinModule {
  def kotlinVersion = "1.9.25"
}

trait AppKotlinJsModule extends AppKotlinModule with KotlinJsModule

object `package` extends RootModule with AppKotlinModule {

  def ktorVersion = "2.3.12"
  def kotlinHtmlVersion = "0.11.0"
  def kotlinxSerializationVersion = "1.6.3"

  def mainClass = Some("webapp.WebApp")

  def moduleDeps = Seq(shared.jvm)

  def ivyDeps = Agg(
    ivy"io.ktor:ktor-server-core-jvm:$ktorVersion",
    ivy"io.ktor:ktor-server-netty-jvm:$ktorVersion",
    ivy"io.ktor:ktor-server-html-builder-jvm:$ktorVersion",
    ivy"io.ktor:ktor-server-content-negotiation-jvm:$ktorVersion",
    ivy"io.ktor:ktor-serialization-kotlinx-json-jvm:$ktorVersion",
    ivy"ch.qos.logback:logback-classic:1.5.8"
  )

  def resources = Task {
    os.makeDir(Task.dest / "webapp")
    val jsPath = client.linkBinary().classes.path
    os.copy(jsPath / "client.js", Task.dest / "webapp/client.js")
    os.copy(jsPath / "client.js.map", Task.dest / "webapp/client.js.map")
    super.resources() ++ Seq(PathRef(Task.dest))
  }

  object test extends KotlinTests with TestModule.Junit5 {
    def ivyDeps = super.ivyDeps() ++ Agg(
      ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1",
      ivy"io.ktor:ktor-server-test-host-jvm:$ktorVersion"
    )
  }

  object shared extends Module {

    trait SharedModule extends AppKotlinModule with PlatformKotlinModule {
      def processors = Task {
        defaultResolver().resolveDeps(
          Agg(
            ivy"org.jetbrains.kotlin:kotlin-serialization-compiler-plugin:${kotlinVersion()}"
          )
        )
      }

      def kotlincOptions = super.kotlincOptions() ++ Seq(
        s"-Xplugin=${processors().head.path}"
      )
    }

    object jvm extends SharedModule {
      def ivyDeps = super.ivyDeps() ++ Agg(
        ivy"org.jetbrains.kotlinx:kotlinx-html-jvm:$kotlinHtmlVersion",
        ivy"org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlinxSerializationVersion"
      )
    }
    object js extends SharedModule with AppKotlinJsModule {
      def ivyDeps = super.ivyDeps() ++ Agg(
        ivy"org.jetbrains.kotlinx:kotlinx-html-js:$kotlinHtmlVersion",
        ivy"org.jetbrains.kotlinx:kotlinx-serialization-json-js:$kotlinxSerializationVersion"
      )
    }
  }

  object client extends AppKotlinJsModule {
    def splitPerModule = false
    def moduleDeps = Seq(shared.js)
    def ivyDeps = Agg(
      ivy"org.jetbrains.kotlinx:kotlinx-html-js:$kotlinHtmlVersion",
      ivy"org.jetbrains.kotlinx:kotlinx-serialization-json-js:$kotlinxSerializationVersion"
    )
  }
}

The JSON serialization logic and HTML generation logic in the shared module is shared between client and server, and uses libraries like kotlinx-serialization and kotlinx-html which work on both Kotlin/JVM and Kotlin/JS. This allows us to freely move code between the client and server, without worrying about what platform or language the code was originally implemented in.

This is a minimal example of shared code compiled to Kotlin/JVM and Kotlin/JS, running on both client and server, meant for illustrating the build configuration. A full exploration of client-server code sharing techniques is beyond the scope of this example.

> ./mill test
...webapp.WebAppTestssimpleRequest ...

> ./mill runBackground

> curl http://localhost:8093
...What needs to be done...
...

> curl http://localhost:8093/static/client.js
...kotlin.js...
...

> ./mill clean runBackground

Spring Boot Hello World App

build.mill (download, browse)
package build
import mill._, kotlinlib._

object `package` extends RootModule with KotlinModule {

  def kotlinVersion = "1.9.24"

  def mainClass = Some("com.example.HelloSpringBootKt")

  def ivyDeps = Agg(
    ivy"org.springframework.boot:spring-boot-starter-web:2.5.6",
    ivy"org.springframework.boot:spring-boot-starter-actuator:2.5.6"
  )

  object test extends KotlinTests with TestModule.Junit5 {
    def ivyDeps = super.ivyDeps() ++ Agg(
      ivy"org.springframework.boot:spring-boot-starter-test:2.5.6"
    )
  }
}

This example demonstrates how to set up a simple Spring Boot webserver, able to handle a single HTTP request at / and reply with a single response.

> mill test
...com.example.HelloSpringBootTest#shouldReturnDefaultMessage() finished...

> mill runBackground

> curl http://localhost:8095
...<h1>Hello, World!</h1>...

> mill clean runBackground

Spring Boot TodoMvc App

build.mill (download, browse)
package build
import mill._, kotlinlib._

object `package` extends RootModule with KotlinModule {
  def kotlinVersion = "1.9.24"

  def mainClass = Some("com.example.TodomvcApplicationKt")
  def ivyDeps = Agg(
    ivy"org.springframework.boot:spring-boot-starter-data-jpa:2.5.4",
    ivy"org.springframework.boot:spring-boot-starter-thymeleaf:2.5.4",
    ivy"org.springframework.boot:spring-boot-starter-validation:2.5.4",
    ivy"org.springframework.boot:spring-boot-starter-web:2.5.4",
    ivy"org.jetbrains.kotlin:kotlin-reflect:2.0.21",
    ivy"javax.xml.bind:jaxb-api:2.3.1",
    ivy"org.webjars:webjars-locator:0.41",
    ivy"org.webjars.npm:todomvc-common:1.0.5",
    ivy"org.webjars.npm:todomvc-app-css:2.4.1"
  )

  trait HelloTests extends KotlinTests with TestModule.Junit5 {
    def mainClass = Some("com.example.TodomvcApplicationKt")
    def ivyDeps = super.ivyDeps() ++ Agg(
      ivy"org.springframework.boot:spring-boot-starter-test:2.5.6"
    )
  }

  object test extends HelloTests {
    def ivyDeps = super.ivyDeps() ++ Agg(
      ivy"com.h2database:h2:2.3.230"
    )
  }

  object integration extends HelloTests {
    def ivyDeps = super.ivyDeps() ++ Agg(
      ivy"org.testcontainers:testcontainers:1.18.0",
      ivy"org.testcontainers:junit-jupiter:1.18.0",
      ivy"org.testcontainers:postgresql:1.18.0",
      ivy"org.postgresql:postgresql:42.6.0"
    )
  }
}

This is a larger example using Spring Boot, implementing the well known TodoMVC example app. Apart from running a webserver, this example also demonstrates:

  • Serving HTML templates using Thymeleaf

  • Serving static Javascript and CSS using Webjars

  • Querying a SQL database using JPA and H2

  • Unit testing using a H2 in-memory database

  • Integration testing using Testcontainers Postgres in Docker

> mill test
...com.example.TodomvcTests#homePageLoads() finished...
...com.example.TodomvcTests#addNewTodoItem() finished...

> mill integration
...com.example.TodomvcIntegrationTests#homePageLoads() finished...
...com.example.TodomvcIntegrationTests#addNewTodoItem() finished...

> mill test.runBackground

> curl http://localhost:8099
...<h1>todos</h1>...

> mill clean runBackground