Aplikasi web yang baik dimulai dari struktur yang jelas: di mana routing didefinisikan, bagaimana feature dipasang, dan dalam urutan apa request mengalir melalui pipeline. Pada Bagian 1 ini kita menyiapkan kerangka backend Ktor dengan pola Application module yang ter modular. Kita menulis kode sebagai Kotlin murni yang dapat dijalankan di terminal, dengan struktur yang sama persis dengan aplikasi Ktor asli.

Application Module #

Konsep inti Ktor adalah Application: kontainer tempat seluruh feature dipasang. Ktor menganjurkan pola Application module function — fungsi extension Application.module() yang menginstall feature satu per satu:

kotlin Struktur Application.module()
fun Application.module() {
    configureLogging()
    configureSerialization()
    configureRouting()
}

Setiap fungsi configureX() menginstall satu feature: CallLogging untuk logging, ContentNegotiation untuk JSON, Routing untuk endpoint. Pemisahan ini membuat kode mudah dibaca dan feature dapat diuji terpisah. Di produksi, kode ini berada di file Application.kt dan dipanggil oleh engine main saat server start.

Feature Installation #

Feature di Ktor dipasang dengan install(NamaFeature) { config }. Pola ini menambahkan feature ke pipeline request. Saat request datang, setiap feature memproses request secara berurutan, dapat memodifikasi, menolak, atau meneruskan ke feature berikutnya.

Implementasi Pipeline Modular #

Kode berikut mendemonstrasikan struktur backend Ktor dalam Kotlin murni: KtorApplication sebagai kontainer, installFeature untuk menambah middleware, routing untuk mendaftar handler, dan handle(call) yang menjalankan pipeline:

kotlin
// Bagian 1: Struktur backend Ktor.
// Mendemonstrasikan pola Application module dengan feature installation
// yang terpisah, routing table, dan pipeline middleware.

data class Rute(
    val method: String,
    val path: String,
    val deskripsi: String
)

data class HttpResponse(
    val status: Int,
    val body: String,
    val contentType: String = "application/json"
)

// === Fitur konfigurasi (mirror dari Ktor: configureSerialization, dst.) ===

interface ApplicationCall {
    val requestMethod: String
    val requestPath: String
    var response: HttpResponse?
}

// Simulasi Application class di Ktor.
class KtorApplication {
    private val features = mutableListOf<(ApplicationCall) -> Boolean>()
    private val routes = mutableListOf<Rute>()
    private val handlers = mutableListOf<(ApplicationCall) -> Boolean>()

    fun installFeature(name: String, handler: (ApplicationCall) -> Boolean) {
        println("  [install] feature: $name")
        features.add(handler)
    }

    fun routing(route: Rute, handler: (ApplicationCall) -> Boolean) {
        routes.add(route)
        handlers.add(handler)
    }

    fun daftarRute(): List<Rute> = routes.toList()

    fun handle(call: ApplicationCall): HttpResponse {
        // Pipeline: jalankan middleware terlebih dahulu
        for (feature in features) {
            if (!feature(call)) {
                return call.response ?: HttpResponse(500, """{"error":"middleware gagal"}""")
            }
        }
        // Routing: cari handler yang cocok
        for (handler in handlers) {
            if (handler(call)) {
                return call.response ?: HttpResponse(200, """{"status":"ok"}""")
            }
        }
        return HttpResponse(404, """{"error":"tidak ditemukan"}""")
    }
}

// === Application.module() ===
fun KtorApplication.module() {
    configureLogging()
    configureSerialization()
    configureRouting()
}

private fun KtorApplication.configureLogging() {
    installFeature("CallLogging") { call ->
        // Log setiap request. Selalu return true agar request lanjut.
        true
    }
}

private fun KtorApplication.configureSerialization() {
    installFeature("ContentNegotiation") { call ->
        // Otomatis parse body JSON. Di Ktor asli:
        // install(ContentNegotiation) { json(Json { ... }) }
        true
    }
}

private fun KtorApplication.configureRouting() {
    routing(Rute("GET", "/api/health", "cek status server")) { call ->
        if (call.requestMethod == "GET" && call.requestPath == "/api/health") {
            call.response = HttpResponse(200, """{"status":"ok"}""")
            true
        } else false
    }
    routing(Rute("GET", "/api/tasks", "daftar tugas")) { call ->
        if (call.requestMethod == "GET" && call.requestPath == "/api/tasks") {
            call.response = HttpResponse(200, """{"tasks":[]}""")
            true
        } else false
    }
}

// Implementasi ApplicationCall sederhana
class SimpleCall(override val requestMethod: String, override val requestPath: String) : ApplicationCall {
    override var response: HttpResponse? = null
}

fun main() {
    println("=== Bootstrap aplikasi ===")
    val app = KtorApplication()
    app.module()

    println()
    println("=== Daftar rute terdaftar ===")
    app.daftarRute().forEach { r ->
        println("  ${r.method.padEnd(7)} ${r.path.padEnd(20)} - ${r.deskripsi}")
    }

    println()
    println("=== Pipeline test ===")
    val requests = listOf(
        SimpleCall("GET", "/api/health"),
        SimpleCall("GET", "/api/tasks"),
        SimpleCall("GET", "/api/unknown"),
        SimpleCall("POST", "/api/tasks")
    )
    requests.forEach { call ->
        val res = app.handle(call)
        println("  ${call.requestMethod} ${call.requestPath} -> ${res.status} ${res.body}")
    }
}

Output yang Dihasilkan:

text
=== Bootstrap aplikasi ===
  [install] feature: CallLogging
  [install] feature: ContentNegotiation

=== Daftar rute terdaftar ===
  GET     /api/health          - cek status server
  GET     /api/tasks           - daftar tugas

=== Pipeline test ===
  GET /api/health -> 200 {"status":"ok"}
  GET /api/tasks -> 200 {"tasks":[]}
  GET /api/unknown -> 404 {"error":"tidak ditemukan"}
  POST /api/tasks -> 404 {"error":"tidak ditemukan"}

Mari kita bedah komponen utama:

  1. class KtorApplication dengan installFeature, routing, handle: cerminan Application Ktor asli. Feature disimpan dalam list of lambdas; handler disimpan dalam list paralel dengan rute. handle(call) menjalankan pipeline: middleware dulu, lalu routing.
  2. interface ApplicationCall: abstraksi permintaan HTTP. Memiliki requestMethod, requestPath, dan response yang dapat diatur oleh handler. Di Ktor asli, ApplicationCall adalah class yang menyediakan akses ke header, query parameter, body, dan respond method.
  3. fun KtorApplication.module(): extension function pada KtorApplication. Ktor asli menggunakan signature yang sama persis: fun Application.module() { ... }. Pemisahan ke configureLogging(), configureSerialization(), configureRouting() membuat kode modular.
  4. installFeature("CallLogging") { call -> ... }: pola feature installation. Lambda menerima call dan mengembalikan Boolean: true untuk melanjutkan ke feature/route berikutnya, false untuk menghentikan pipeline (misalnya, authentication yang menolak mengembalikan false).
  5. for (handler in handlers) { if (handler(call)) return call.response }: routing sequential. Ktor asli menggunakan struktur data yang lebih efisien (trie), tetapi konsepnya sama: cari handler pertama yang cocok dengan method + path.

Routing DSL #

Ktor asli menyediakan DSL yang ringkas untuk routing:

kotlin Routing DSL Ktor asli
routing {
    route("/api") {
        get("/health") { call.respond(mapOf("status" to "ok")) }
        get("/tasks") { call.respond(mapOf("tasks" to emptyList<String>())) }
        post("/tasks") { /* ... */ }
    }
}

DSL ini menggunakan receiver lambda Routing.() -> Unit yang memungkinkan nested route { get { } } dengan sintaks yang terbaca seperti deklarasi endpoint. Di balik layar, Ktor membangun trie untuk pencocokan path yang efisien.

Middleware dan Pipeline #

Middleware (disebut “feature” atau “plugin” di Ktor) memproses setiap request sebelum routing. Contoh:

  • CallLogging: log method, path, status, dan waktu response ke console.
  • ContentNegotiation: parse body JSON menjadi object Kotlin otomatis.
  • StatusPages: tangkap exception dan ubah menjadi response 500 dengan body JSON.
  • Compression: kompres response dengan gzip jika klien mendukung.

Urutan instalasi feature menentukan urutan eksekusi. Logging biasanya dipasang paling awal agar mencatat semua request; authentication dipasang sebelum routing agar dapat menolak request sebelum mencapai handler.

Konfigurasi via HOCON #

Ktor menggunakan HOCON (application.conf) untuk konfigurasi:

text application.conf
ktor {
    deployment {
        port = 8080
        port = ${?PORT}
    }
    application {
        modules = [ com.example.ApplicationKt.module ]
    }
}

${?PORT} membaca dari environment variable dengan fallback ke 8080 jika tidak diset. Modul diaktifkan via modules list. Pola ini memungkinkan konfigurasi berbeda antara dev, staging, dan produksi tanpa mengubah kode.

Referensi Lanjutan #

Ringkasan Bagian 1 #

Struktur backend kini siap. KtorApplication memegang feature dan routing; module() menginstall tiga feature modular: CallLogging, ContentNegotiation, dan Routing. Pipeline menjalankan middleware dulu, lalu handler. Pada Bagian 2 kita menambahkan endpoint REST API dengan parsing body JSON dan validasi request yang terstruktur.