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.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.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.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
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.
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.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.
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