以下の内容はhttps://blog.inorinrinrin.com/entry/2025/11/01/140458より取得しました。


Ktorを使う時に設定しておきたいこと

Kotlin Fest 2025の登壇資料です

以下補足

はんずおんExposed

ExposedはKotlin向けに作られたJetBrains社製のORM。Ktor×Exposedでのバックエンドアプリケーションの導入事例はほとんど見られず、Exposedに関するノウハウがあまり出回っていない印象がある。

事実、プロシージャやらファンクションやらcaseやら複雑なサブクエリやらを使ったSQLをExposedに移植しようとした際に非常に困った。なぜなら、この辺のノウハウは当時のExposed Wikiにはしっかり書かれておらず、ソースコードを解析するしかなかったので。

とまあそんな悲しい歴史を繰り返さないために執筆した本がこちら。

techbookfest.org

iolite

github.com

日付、メールアドレス、IPアドレスなど仕様が必ず決まっていて、かつ世界のいろんな場所で繰り返し使われることになる値オブジェクトを集めたライブラリになっている。入力をparseすることでエラーを吐いたり、エラーという結果だけを受け取って呼び出し元で後処理をコントロールできるようになっている。

parse

try {
  val email: String = Email("youremail@example.com").parse()
}catch (e: IllegalArgumentException) {
  // Handle the exception if needed
}

safeParse

val email = Email("youremail@example.com").safeParse()
if(email.isFailure){
  // Handle the exception if needed
}
println(email.getOrNull()) // print "youremail@example.com"

Extending Ktor for Server Side Development | Ido Flax

2025年(今年)のKotlin Conf発表。Pluginの作成などについても言及していて、めちゃくちゃいい発表。

www.youtube.com

Auto Reload

qiita.com

application.yml

ktor:
    application:
        modules:
            - com.example.ApplicationKt.module
+    development: true
    deployment:
        port: 8080
+        watch:
+            - classes

変更時に再コンパイルされるように設定する

./gradlew -t build -x test -i

検証

Routing.ktを修正

package com.example

import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.jetbrains.exposed.sql.*
import org.koin.dsl.module
import org.koin.ktor.plugin.Koin
import org.koin.logger.slf4jLogger

fun Application.configureRouting() {
    routing {
        get("/") {
-            call.respondText("Hello Ktor!!")
+            call.respondText("Hello Ktor!! Reloaded!!")
        }
    }
}

Call ID

qiita.com

Application.kt

package example.koin

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.callid.*
import io.ktor.server.plugins.callloging.*

fun main(args: Array<String>) {
    io.ktor.server.tomcat.EngineMain.main(args)
}

fun generateRandomString(length: Int): String {
    val charPool = ('a'..'z') + ('A'..'Z') + ('0'..'9') // 小文字、大文字、数字
    return (1..length)
        .map { Random.nextInt(charPool.size) }
        .map(charPool::get)
        .joinToString("")
}

fun Application.module() {
    settingKoin()
    configureRouting()
    install(CallId) {
        header(HttpHeaders.XRequestId)
        generate { generateRandomString(10) }
        verify { callId: String ->
            callId.isNotEmpty()
        }
    }
    install(CallLogging) {
        callIdMdc("call-id")
    }
}

logback.xml

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %X{call-id} %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="debug">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

Type-Safe Routing

qiita.com

散らかり気味のRouting.kt

めちゃくちゃ巨大

package example.koin

import example.koin.controller.ExposedController
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.inject

fun Application.configureRouting() {
    val exposedController by inject<ExposedController>()
    routing {
        get("/") {
            call.respondText("Hello! hands on exposed!!")
        }
        get("/inorin"){
            exposedController.getInorin(call)
        }
        get("/allEmployees"){
            exposedController.getAllEmployees(call)
        }
        get("/allEmployeesNames"){
            exposedController.getAllEmployeesNames(call)
        }
        get("/employeeNameOfGeneralOrAccounting"){
            exposedController.getEmployeeNameOfGeneralOrAccounting(call)
        }
        get("/employeeBySorted"){
            exposedController.getEmployeeBySorted(call)
        }
        get("/howManyApplyExpenseByEmployee"){
            exposedController.getHowManyApplyExpenseByEmployee(call)
        }
        get("/howMuchExpenseByEmployee"){
            exposedController.getHowMuchExpenseByEmployee(call)
        }
        get("/employeeLimitOffset"){
            exposedController.getEmployeeLimitOffset(call)
        }
        get("/employeeNameAndDepartment"){
            exposedController.getEmployeeNameAndDepartment(call)
        }
        get("/hasExpenseEmployeeNames"){
            exposedController.hasExpenseEmployeeNames(call)
        }
        get("/hasExpenseEmployeeNamesWithBetween"){
            exposedController.hasExpenseEmployeeNamesWithBetween(call)
        }
        get("/allEmployeeTypeAndNames"){
            exposedController.getAllEmployeeTypeAndNames(call)
        }
        get("/allEmployeeTypeAndNamesDistinct"){
            exposedController.getAllEmployeeTypeAndNamesDistinct(call)
        }
        get("/hasExpenseEmployeeIdAndNames"){
            exposedController.getHasExpenseEmployeeIdAndNames(call)
        }
        get("/overExpenseEmployeeIdAndNames"){
            exposedController.getOverExpenseEmployeeIdAndNames(call)
        }
        get("/existsOverExpenseEmployeeIdAndNames"){
            exposedController.getExistsOverExpenseEmployeeIdAndNames(call)
        }
        get("/latestEmployeeIdByDepartmentId"){
            exposedController.getLatestEmployeeIdByDepartmentId(call)
        }
        get("/employeeNamesAndEnrollmentStatus"){
            exposedController.getEmployeeNamesAndEnrollmentStatus(call)
        }
        get("/concatEmployeeNames"){
            exposedController.getConcatEmployeeNames(call)
        }
        get("/concatPartnerNames"){
            exposedController.getConcatPartnerNames(call)
        }
        get("/employeeFirstNameStrByte"){
            exposedController.getEmployeeFirstNameStrByte(call)
        }
        get("/employeeFirstNameCharLength"){
            exposedController.getEmployeeFirstNameCharLength(call)
        }
        get("/employeeFirstNameCharLengthOver3"){
            exposedController.getEmployeeFirstNameCharLengthOver3(call)
        }
        post("/insertUpdateDeleteEmployee"){
            exposedController.insertUpdateDeleteEmployee(call)
        }
        put("/updateApplyExpenseEmployee"){
            exposedController.updateApplyExpenseEmployee(call)
        }
        get("/allPartners"){
            exposedController.getAllPartners(call)
        }
        get("/allPartnersNames"){
            exposedController.getAllPartnersNames(call)
        }
        get("/partnerNameById"){
            exposedController.getPartnerNameById(call)
        }
        get("/partnerNameByLikeKeyword"){
            exposedController.getAllPartnersNamesByLikeKeyword(call)
        }
        get("/partnerBySorted"){
            exposedController.getPartnerBySorted(call)
        }
        get("/partnerLimitOffset"){
            exposedController.getPartnerLimitOffset(call)
        }
        get("/partnerNameAndDepartment"){
            exposedController.getPartnerNameAndDepartment(call)
        }
        get("/partnerNamesAndEnrollmentStatus"){
            exposedController.getPartnerNamesAndEnrollmentStatus(call)
        }
        post("/insertUpdateDeletePartner"){
            exposedController.insertUpdateDeletePartner(call)
        }
    }
}

リソースクラスを作成

  • Partner関係はここに全部入れていく
  • Pathは少し修正が必要かもしれない
package example.koin.resources

import io.ktor.resources.*

@Resource("/partners")
class Partner {
    @Resource("all")
    class all(val parent: Partner)

    @Resource("names")
    class names(val parent: Partner){
        @Resource("all")
        class all(val parent: names)

        @Resource("{id}")
        class byId(val parent: names, val id: Int)
    }
}

Routing.kt 修正前後の比較

        get("/allPartners"){ // before
            exposedController.getAllPartners(call)
        }
        get<Partner.all>{ // after
            exposedController.getAllPartners(call)
        }
        get("/allPartnersNames"){ // before
            exposedController.getAllPartnersNames(call)
        }
        get<Partner.names.all>{ // after
            exposedController.getAllPartnersNames(call)
        }
        get("/partnerNameById"){ // before
            val id = call.parameters["partnerId"]?.toIntOrNull() ?: -1
            exposedController.getPartnerNameById(call, id)
        }
        get<Partner.names.byId>{ // after
            partner -> exposedController.getPartnerNameById(call, partner.id)
        }

Status Pages

ktor.io

package com.example

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

fun Application.module() {
    install(StatusPages) {
        exception<Throwable> { call, cause ->
            if(cause is AuthorizationException) {
                call.respondText(text = "403: $cause" , status = HttpStatusCode.Forbidden)
            } else {
                call.respondText(text = "500: $cause" , status = HttpStatusCode.InternalServerError)
            }
        }
        status(HttpStatusCode.NotFound) { call, status ->
            call.respondText(text = "404: Page Not Found", status = status)
        }
        statusFile(HttpStatusCode.Unauthorized, HttpStatusCode.PaymentRequired, filePattern = "error#.html")
    }
    routing {
        get("/") {
            call.respondText("Hello, world!")
        }
        get("/internal-error") {
            throw Exception("Internal Server Error")
        }
        get("/authorization-error") {
            throw AuthorizationException("Forbidden Error")
        }
        get("/authentication-error") {
            call.respond(HttpStatusCode.Unauthorized)
        }
        get("/payment-error") {
            call.respond(HttpStatusCode.PaymentRequired)
        }
    }
}

class AuthorizationException(override val message: String?) : Throwable()

https://github.com/ktorio/ktor-documentation/blob/main/codeSnippets/snippets/status-pages/src/main/kotlin/com/example/Application.kt

Request Validation

ktor.io

package com.example

import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.requestvalidation.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.*

@Serializable
data class Customer(val id: Int, val firstName: String, val lastName: String)

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

fun Application.module() {
    install(RequestValidation) {
        validate<String> { bodyText ->
            if (!bodyText.startsWith("Hello"))
                ValidationResult.Invalid("Body text should start with 'Hello'")
            else ValidationResult.Valid
        }
        validate<Customer> { customer ->
            if (customer.id <= 0)
                ValidationResult.Invalid("A customer ID should be greater than 0")
            else ValidationResult.Valid
        }
        validate {
            filter { body ->
                body is ByteArray
            }
            validation { body ->
                check(body is ByteArray)
                val intValue = String(body).toInt()
                if (intValue <= 0)
                    ValidationResult.Invalid("A value should be greater than 0")
                else ValidationResult.Valid
            }
        }
    }
    install(StatusPages) {
        exception<RequestValidationException> { call, cause ->
            call.respond(HttpStatusCode.BadRequest, cause.reasons.joinToString())
        }
    }
    install(ContentNegotiation) {
        json()
    }
    routing {
        post("/text") {
            val body = call.receive<String>()
            call.respond(body)
        }
        post("/json") {
            val customer = call.receive<Customer>()
            call.respond(customer)
        }
        post("/array") {
            val body = call.receive<ByteArray>()
            call.respond(String(body))
        }
    }
}

https://github.com/ktorio/ktor-documentation/blob/main/codeSnippets/snippets/request-validation/src/main/kotlin/com/example/Application.kt

さらっと触れた、他入れときたいPlugin

ktor.io

ktor.io

ktor.io

ktor.io

github.com

竹端さんの資料

speakerdeck.com

Klibs.io

意外と知らない人が多い?DIとかTestとかカテゴリなどを絞ってGitHubでStarが多くついているライブラリを検索できるサービス。JetBrainsが公式提供している。

klibs.io

2025年(今年)のKotlin ConfでKlibs開発秘話が話されていて、これもめちゃくちゃ面白いので興味ある人は是非。

www.youtube.com




以上の内容はhttps://blog.inorinrinrin.com/entry/2025/11/01/140458より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14