Struktur proyek yang konsisten membuat onboarding tim lebih cepat dan konfigurasi lebih mudah diaudit. Ktor, meski ringan, memiliki konvensi struktur proyek yang jelas: Gradle Kotlin DSL sebagai build tool, file application.conf berformat HOCON untuk konfigurasi runtime, dan layout source code berbasis package. Pada chapter ini kita akan membedah struktur tersebut dan menjalankan program yang memodelkan parsing application.conf.

Anatomi Proyek Ktor #

Proyek Ktor standar memiliki struktur direktori seperti berikut:

text
my-app/
├── build.gradle.kts
├── settings.gradle.kts
├── gradle.properties
├── src/
│   └── main/
│       ├── kotlin/
│       │   └── com/example/
│       │       ├── Application.kt
│       │       ├── plugins/
│       │       │   ├── Routing.kt
│       │       │   ├── Serialization.kt
│       │       │   └── Security.kt
│       │       └── routes/
│       │           └── UserRoutes.kt
│       └── resources/
│           ├── application.conf
│           └── logback.xml

File Application.kt berisi fungsi fun main() yang menjadi entry point. Folder plugins/ mengelompokkan kode pemasangan plugin agar mudah ditemukan. Folder routes/ memisahkan definisi route berdasarkan domain.

Build Gradle Kotlin DSL #

build.gradle.kts proyek Ktor memakai plugin application dan kotlin-jvm. Berikut ringkasan konfigurasi inti:

kotlin build.gradle.kts
plugins {
    kotlin("jvm") version "2.0.21"
    application
    kotlin("plugin.serialization") version "2.0.21"
}

application {
    mainClass.set("com.example.ApplicationKt")
}

dependencies {
    implementation("io.ktor:ktor-server-core:2.3.12")
    implementation("io.ktor:ktor-server-netty:2.3.12")
    implementation("io.ktor:ktor-server-content-negotiation:2.3.12")
    implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")
    testImplementation("io.ktor:ktor-server-tests:2.3.12")
}

Versi Ktor 2.3.x stabil untuk produksi. Plugin kotlin("plugin.serialization") mengaktifkan kompilator @Serializable yang akan dibahas tuntas pada chapter Ktor Serialization.

application.conf (HOCON) #

Ktor memakai format HOCON (Human-Optimized Config Object Notation) untuk application.conf. File ini menentukan port, host, environment, dan modul mana yang dimuat saat server start. Berikut contoh application.conf produksi:

hocon application.conf
ktor {
    deployment {
        port = 8080
        port = ${?PORT}
        host = 0.0.0.0
        watch = ["classes"]
    }
    application {
        modules = [ com.example.ApplicationKt.module ]
    }
}

Sintaks ${?PORT} artinya nilai dari environment variable PORT jika ada; jika tidak ada, fallback ke 8080. Sintaks modules = [ ... ] menentukan function module mana yang dipanggil Ktor saat bootstrap — biasanya extension function Application.module().

Demo Parsing application.conf #

Karena Ktor nyata memerlukan dependensi eksternal, kita akan memodelkan parsing application.conf sebagai program Kotlin biasa. Program ini membaca string HOCON sederhana dan menghasilkan objek KtorConfig yang merepresentasikan konfigurasi yang sudah di-parse.

kotlin
// ktor_project_structure.kt
// Mendemonstrasikan struktur proyek Ktor: main class Application,
// application.conf HOCON, dan koneksi modul. Pada kode produksi, file
// application.conf dibaca engine Ktor. Di sini kita memodelkannya.

import java.util.Properties

// Merepresentasikan konfigurasi dari application.conf (HOCON)
data class KtorConfig(
    val module: String,
    val host: String,
    val port: Int,
    val development: Boolean
) {
    companion object {
        fun fromHocon(hocon: String): KtorConfig {
            // Parser HOCON sederhana untuk demo
            val props = Properties().apply { load(hocon.byteInputStream()) }
            val module = hocon.substringAfter("module = ").substringBefore("\n").trim()
            val host = hocon.substringAfter("host = ").substringBefore("\n").trim()
            val port = hocon.substringAfter("port = ").substringBefore("\n").trim().toInt()
            val dev = hocon.substringAfter("development = ").substringBefore("\n").trim().toBoolean()
            return KtorConfig(module, host, port, dev)
        }
    }
}

// Application module: fungsi yang menerima Application dan memasang plugin/route
interface ApplicationModule {
    val nama: String
    fun apply(app: Application)
}

// Mock Application ringkas
class Application {
    val routes = mutableMapOf<String, String>()
    fun get(path: String, body: String) { routes[path] = body }
}

// Entry point: mirip com.example.ApplicationKt.main() di Ktor
fun main(args: Array<String>) {
    val hoconConf = """
        module = com.example.ApplicationKt
        host = 0.0.0.0
        port = 8080
        development = true
    """.trimIndent()

    println("=== application.conf (HOCON) ===")
    println(hoconConf)

    val config = KtorConfig.fromHocon(hoconConf)
    println("\n=== Hasil parsing ===")
    println(config)

    println("\n=== Module loading ===")
    val module = object : ApplicationModule {
        override val nama = config.module
        override fun apply(app: Application) {
            app.get("/", "Halo dari ${config.module}")
            app.get("/health", "OK")
        }
    }

    val app = Application()
    module.apply(app)

    println("Module: ${module.nama}")
    println("Routes: ${app.routes}")
    println("Environment: ${if (config.development) "dev" else "prod"} di ${config.host}:${config.port}")
}

Output yang Dihasilkan:

text
=== application.conf (HOCON) ===
module = com.example.ApplicationKt
host = 0.0.0.0
port = 8080
development = true

=== Hasil parsing ===
KtorConfig(module=com.example.ApplicationKt, host=0.0.0.0, port=8080, development=true)

=== Module loading ===
Module: com.example.ApplicationKt
Routes: {/=Halo dari com.example.ApplicationKt, /health=OK}
Environment: dev di 0.0.0.0:8080

Mari kita bedah bagian penting kode di atas:

  1. data class KtorConfig: model konfigurasi dengan empat field. Companion object menyimpan factory fromHocon yang mengubah string HOCON menjadi instance.
  2. hocon.substringAfter("port = ").substringBefore("\n").trim().toInt(): parser sederhana berbasis string. Kode produksi memakai library hocon resmi (typesafe config) yang menangani nesting dan escape dengan benar.
  3. interface ApplicationModule { fun apply(app: Application) }: kontrak modul — function yang menerima Application dan memasang plugin/route. Di Ktor nyata, ini adalah extension function Application.module().
  4. object : ApplicationModule { ... }: anonymous object yang mengimplementasi modul untuk demo. Modul ini mendaftarkan dua route dan mengembalikan body statis.
  5. println("Routes: ${app.routes}"): cetak peta route setelah modul diterapkan. Output memperlihatkan korelasi antara konfigurasi HOCON, modul, dan route.

Konvensi Penamaan dan Source Layout #

Package utama biasanya com.<org>.<app>. File Application.kt diletakkan di root package agar path main class pendek (com.example.ApplicationKt). Plugin dipisah ke folder plugins/ dan route ke folder routes/ — pemisahan ini bukan keharusan kompilator, tetapi memudahkan navigasi pada proyek dengan puluhan file.

File logback.xml di folder resources/ mengatur logging. Ktor memakai SLF4J sebagai facade dan Logback sebagai implementasi default. Konfigurasi log level, appender, dan pattern ditulis dalam XML di file ini.

Referensi Lanjutan #

Selanjutnya #

Setelah memahami struktur proyek Ktor, langkah berikutnya adalah mendaftarkan route. Chapter Ktor Routing akan membahas DSL routing { get("/users") { } }, route parameter dinamis, dan nested routes yang menjadi inti aplikasi web.