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
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"
)
}
}
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>")
}
}
}
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 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.
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:
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
.
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.WebAppTests simpleRequest ...
> ./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.
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.WebAppTests simpleRequest ...
> ./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
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
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