Kotlin Ktor Patterns

作者 affaan-mef648e01899b無授權條款275K 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫3 天前更新

Ktor サーバーパターン(ルーティング DSL、プラグイン、認証、Koin DI、kotlinx.serialization、WebSocket、testApplication テストを含む)。

AI 產生的概覽

用於在 Kotlin 中建構 Ktor HTTP 伺服器的參考模式,涵蓋路由、外掛、驗證、相依性注入、序列化、WebSocket 與測試。

功能
提供一套結構化的 Ktor 伺服器模式參考,包括專案結構、路由 DSL、以 kotlinx.serialization 進行的內容協商、JWT 驗證、StatusPages 錯誤處理、CORS、Koin 相依性注入、請求驗證、WebSocket 以及 testApplication 整合測試。也包含 application.yaml 設定範例與常用 Ktor 呼叫的速查表。產出的是指引說明與程式碼片段,而非可直接執行的成品。
適用情境
適用於建構或維護 Ktor HTTP 伺服器、設定 Ktor 外掛、實作 REST API、設定 Koin 相依性注入、加入 WebSocket 支援,或撰寫 Kotlin 的 testApplication 整合測試。
執行需求
不含指令碼或可執行檔,僅有說明與程式碼範例。套用這些模式需要具備 Ktor、kotlinx.serialization、Koin、JWT 及相關測試函式庫的 Kotlin 專案,以及 JWT 密鑰、資料庫 URL 等設定值。

Ktor サーバーパターン

Kotlin コルーチンで堅牢かつ保守性の高い HTTP サーバーを構築するための包括的な Ktor パターン。

アクティベートするタイミング

  • Ktor HTTP サーバーの構築
  • Ktor プラグインの設定(Auth、CORS、ContentNegotiation、StatusPages)
  • Ktor を使用した REST API の実装
  • Koin を使用した依存性注入の設定
  • testApplication を使用した Ktor インテグレーションテストの作成
  • Ktor での WebSocket の使用

アプリケーション構造

標準的な Ktor プロジェクトレイアウト

text
src/main/kotlin/├── com/example/│   ├── Application.kt           # エントリーポイント、モジュール設定│   ├── plugins/│   │   ├── Routing.kt           # ルート定義│   │   ├── Serialization.kt     # コンテントネゴシエーション設定│   │   ├── Authentication.kt    # 認証設定│   │   ├── StatusPages.kt       # エラーハンドリング│   │   └── CORS.kt              # CORS 設定│   ├── routes/│   │   ├── UserRoutes.kt        # /users エンドポイント│   │   ├── AuthRoutes.kt        # /auth エンドポイント│   │   └── HealthRoutes.kt      # /health エンドポイント│   ├── models/│   │   ├── User.kt              # ドメインモデル│   │   └── ApiResponse.kt       # レスポンスエンベロープ│   ├── services/│   │   ├── UserService.kt       # ビジネスロジック│   │   └── AuthService.kt       # 認証ロジック│   ├── repositories/│   │   ├── UserRepository.kt    # データアクセスインターフェース│   │   └── ExposedUserRepository.kt│   └── di/│       └── AppModule.kt         # Koin モジュールsrc/test/kotlin/├── com/example/│   ├── routes/│   │   └── UserRoutesTest.kt│   └── services/│       └── UserServiceTest.kt

アプリケーションエントリーポイント

kotlin
// Application.ktfun main() {    embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)}
fun Application.module() {    configureSerialization()    configureAuthentication()    configureStatusPages()    configureCORS()    configureDI()    configureRouting()}

ルーティング DSL

基本ルート

kotlin
// plugins/Routing.ktfun Application.configureRouting() {    routing {        userRoutes()        authRoutes()        healthRoutes()    }}
// routes/UserRoutes.ktfun Route.userRoutes() {    val userService by inject<UserService>()
    route("/users") {        get {            val users = userService.getAll()            call.respond(users)        }
        get("/{id}") {            val id = call.parameters["id"]                ?: return@get call.respond(HttpStatusCode.BadRequest, "Missing id")            val user = userService.getById(id)                ?: return@get call.respond(HttpStatusCode.NotFound)            call.respond(user)        }
        post {            val request = call.receive<CreateUserRequest>()            val user = userService.create(request)            call.respond(HttpStatusCode.Created, user)        }
        put("/{id}") {            val id = call.parameters["id"]                ?: return@put call.respond(HttpStatusCode.BadRequest, "Missing id")            val request = call.receive<UpdateUserRequest>()            val user = userService.update(id, request)                ?: return@put call.respond(HttpStatusCode.NotFound)            call.respond(user)        }
        delete("/{id}") {            val id = call.parameters["id"]                ?: return@delete call.respond(HttpStatusCode.BadRequest, "Missing id")            val deleted = userService.delete(id)            if (deleted) call.respond(HttpStatusCode.NoContent)            else call.respond(HttpStatusCode.NotFound)        }    }}

認証ルートを使用したルート整理

kotlin
fun Route.userRoutes() {    route("/users") {        // パブリックルート        get { /* ユーザー一覧 */ }        get("/{id}") { /* ユーザー取得 */ }
        // 保護されたルート        authenticate("jwt") {            post { /* ユーザー作成 - 認証が必要 */ }            put("/{id}") { /* ユーザー更新 - 認証が必要 */ }            delete("/{id}") { /* ユーザー削除 - 認証が必要 */ }        }    }}

コンテントネゴシエーションとシリアライゼーション

kotlinx.serialization セットアップ

kotlin
// plugins/Serialization.ktfun Application.configureSerialization() {    install(ContentNegotiation) {        json(Json {            prettyPrint = true            isLenient = false            ignoreUnknownKeys = true            encodeDefaults = true            explicitNulls = false        })    }}

シリアライズ可能なモデル

kotlin
@Serializabledata class UserResponse(    val id: String,    val name: String,    val email: String,    val role: Role,    @Serializable(with = InstantSerializer::class)    val createdAt: Instant,)
@Serializabledata class CreateUserRequest(    val name: String,    val email: String,    val role: Role = Role.USER,)
@Serializabledata class ApiResponse<T>(    val success: Boolean,    val data: T? = null,    val error: String? = null,) {    companion object {        fun <T> ok(data: T): ApiResponse<T> = ApiResponse(success = true, data = data)        fun <T> error(message: String): ApiResponse<T> = ApiResponse(success = false, error = message)    }}
@Serializabledata class PaginatedResponse<T>(    val data: List<T>,    val total: Long,    val page: Int,    val limit: Int,)

カスタムシリアライザー

kotlin
object InstantSerializer : KSerializer<Instant> {    override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)    override fun serialize(encoder: Encoder, value: Instant) =        encoder.encodeString(value.toString())    override fun deserialize(decoder: Decoder): Instant =        Instant.parse(decoder.decodeString())}

認証

JWT 認証

kotlin
// plugins/Authentication.ktfun Application.configureAuthentication() {    val jwtSecret = environment.config.property("jwt.secret").getString()    val jwtIssuer = environment.config.property("jwt.issuer").getString()    val jwtAudience = environment.config.property("jwt.audience").getString()    val jwtRealm = environment.config.property("jwt.realm").getString()
    install(Authentication) {        jwt("jwt") {            realm = jwtRealm            verifier(                JWT.require(Algorithm.HMAC256(jwtSecret))                    .withAudience(jwtAudience)                    .withIssuer(jwtIssuer)                    .build()            )            validate { credential ->                if (credential.payload.audience.contains(jwtAudience)) {                    JWTPrincipal(credential.payload)                } else {                    null                }            }            challenge { _, _ ->                call.respond(HttpStatusCode.Unauthorized, ApiResponse.error<Unit>("Invalid or expired token"))            }        }    }}
// JWT からユーザーを取得fun ApplicationCall.userId(): String =    principal<JWTPrincipal>()        ?.payload        ?.getClaim("userId")        ?.asString()        ?: throw AuthenticationException("No userId in token")

認証ルート

kotlin
fun Route.authRoutes() {    val authService by inject<AuthService>()
    route("/auth") {        post("/login") {            val request = call.receive<LoginRequest>()            val token = authService.login(request.email, request.password)                ?: return@post call.respond(                    HttpStatusCode.Unauthorized,                    ApiResponse.error<Unit>("Invalid credentials"),                )            call.respond(ApiResponse.ok(TokenResponse(token)))        }
        post("/register") {            val request = call.receive<RegisterRequest>()            val user = authService.register(request)            call.respond(HttpStatusCode.Created, ApiResponse.ok(user))        }
        authenticate("jwt") {            get("/me") {                val userId = call.userId()                val user = authService.getProfile(userId)                call.respond(ApiResponse.ok(user))            }        }    }}

StatusPages(エラーハンドリング)

kotlin
// plugins/StatusPages.ktfun Application.configureStatusPages() {    install(StatusPages) {        exception<ContentTransformationException> { call, cause ->            call.respond(                HttpStatusCode.BadRequest,                ApiResponse.error<Unit>("Invalid request body: ${cause.message}"),            )        }
        exception<IllegalArgumentException> { call, cause ->            call.respond(                HttpStatusCode.BadRequest,                ApiResponse.error<Unit>(cause.message ?: "Bad request"),            )        }
        exception<AuthenticationException> { call, _ ->            call.respond(                HttpStatusCode.Unauthorized,                ApiResponse.error<Unit>("Authentication required"),            )        }
        exception<AuthorizationException> { call, _ ->            call.respond(                HttpStatusCode.Forbidden,                ApiResponse.error<Unit>("Access denied"),            )        }
        exception<NotFoundException> { call, cause ->            call.respond(                HttpStatusCode.NotFound,                ApiResponse.error<Unit>(cause.message ?: "Resource not found"),            )        }
        exception<Throwable> { call, cause ->            call.application.log.error("Unhandled exception", cause)            call.respond(                HttpStatusCode.InternalServerError,                ApiResponse.error<Unit>("Internal server error"),            )        }
        status(HttpStatusCode.NotFound) { call, status ->            call.respond(status, ApiResponse.error<Unit>("Route not found"))        }    }}

CORS 設定

kotlin
// plugins/CORS.ktfun Application.configureCORS() {    install(CORS) {        allowHost("localhost:3000")        allowHost("example.com", schemes = listOf("https"))        allowHeader(HttpHeaders.ContentType)        allowHeader(HttpHeaders.Authorization)        allowMethod(HttpMethod.Put)        allowMethod(HttpMethod.Delete)        allowMethod(HttpMethod.Patch)        allowCredentials = true        maxAgeInSeconds = 3600    }}

Koin 依存性注入

モジュール定義

kotlin
// di/AppModule.ktval appModule = module {    // データベース    single<Database> { DatabaseFactory.create(get()) }
    // リポジトリ    single<UserRepository> { ExposedUserRepository(get()) }    single<OrderRepository> { ExposedOrderRepository(get()) }
    // サービス    single { UserService(get()) }    single { OrderService(get(), get()) }    single { AuthService(get(), get()) }}
// アプリケーションセットアップfun Application.configureDI() {    install(Koin) {        modules(appModule)    }}

ルートでの Koin の使用

kotlin
fun Route.userRoutes() {    val userService by inject<UserService>()
    route("/users") {        get {            val users = userService.getAll()            call.respond(ApiResponse.ok(users))        }    }}

テスト用の Koin

kotlin
class UserServiceTest : FunSpec(), KoinTest {    override fun extensions() = listOf(KoinExtension(testModule))
    private val testModule = module {        single<UserRepository> { mockk() }        single { UserService(get()) }    }
    private val repository by inject<UserRepository>()    private val service by inject<UserService>()
    init {        test("getUser returns user") {            coEvery { repository.findById("1") } returns testUser            service.getById("1") shouldBe testUser        }    }}

リクエストバリデーション

kotlin
// ルートでリクエストデータを検証fun Route.userRoutes() {    val userService by inject<UserService>()
    post("/users") {        val request = call.receive<CreateUserRequest>()
        // バリデーション        require(request.name.isNotBlank()) { "Name is required" }        require(request.name.length <= 100) { "Name must be 100 characters or less" }        require(request.email.matches(Regex(".+@.+\\..+"))) { "Invalid email format" }
        val user = userService.create(request)        call.respond(HttpStatusCode.Created, ApiResponse.ok(user))    }}
// またはバリデーション拡張を使用fun CreateUserRequest.validate() {    require(name.isNotBlank()) { "Name is required" }    require(name.length <= 100) { "Name must be 100 characters or less" }    require(email.matches(Regex(".+@.+\\..+"))) { "Invalid email format" }}

WebSocket

kotlin
fun Application.configureWebSockets() {    install(WebSockets) {        pingPeriod = 15.seconds        timeout = 15.seconds        maxFrameSize = 64 * 1024 // 64 KiB — プロトコルがより大きなフレームを必要とする場合のみ増加        masking = false // RFC 6455 に従い、サーバーからクライアントへのフレームはマスクなし。クライアントからサーバーは Ktor が常にマスク    }}
fun Route.chatRoutes() {    val connections = Collections.synchronizedSet<Connection>(LinkedHashSet())
    webSocket("/chat") {        val thisConnection = Connection(this)        connections += thisConnection
        try {            send("Connected! Users online: ${connections.size}")
            for (frame in incoming) {                frame as? Frame.Text ?: continue                val text = frame.readText()                val message = ChatMessage(thisConnection.name, text)
                // ConcurrentModificationException を避けるためにロック下でスナップショットを作成                val snapshot = synchronized(connections) { connections.toList() }                snapshot.forEach { conn ->                    conn.session.send(Json.encodeToString(message))                }            }        } catch (e: Exception) {            logger.error("WebSocket error", e)        } finally {            connections -= thisConnection        }    }}
data class Connection(val session: DefaultWebSocketSession) {    val name: String = "User-${counter.getAndIncrement()}"
    companion object {        private val counter = AtomicInteger(0)    }}

testApplication テスト

基本的なルートテスト

kotlin
class UserRoutesTest : FunSpec({    test("GET /users returns list of users") {        testApplication {            application {                install(Koin) { modules(testModule) }                configureSerialization()                configureRouting()            }
            val response = client.get("/users")
            response.status shouldBe HttpStatusCode.OK            val body = response.body<ApiResponse<List<UserResponse>>>()            body.success shouldBe true            body.data.shouldNotBeNull().shouldNotBeEmpty()        }    }
    test("POST /users creates a user") {        testApplication {            application {                install(Koin) { modules(testModule) }                configureSerialization()                configureStatusPages()                configureRouting()            }
            val client = createClient {                install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) {                    json()                }            }
            val response = client.post("/users") {                contentType(ContentType.Application.Json)                setBody(CreateUserRequest("Alice", "[email protected]"))            }
            response.status shouldBe HttpStatusCode.Created        }    }
    test("GET /users/{id} returns 404 for unknown id") {        testApplication {            application {                install(Koin) { modules(testModule) }                configureSerialization()                configureStatusPages()                configureRouting()            }
            val response = client.get("/users/unknown-id")
            response.status shouldBe HttpStatusCode.NotFound        }    }})

認証ルートのテスト

kotlin
class AuthenticatedRoutesTest : FunSpec({    test("protected route requires JWT") {        testApplication {            application {                install(Koin) { modules(testModule) }                configureSerialization()                configureAuthentication()                configureRouting()            }
            val response = client.post("/users") {                contentType(ContentType.Application.Json)                setBody(CreateUserRequest("Alice", "[email protected]"))            }
            response.status shouldBe HttpStatusCode.Unauthorized        }    }
    test("protected route succeeds with valid JWT") {        testApplication {            application {                install(Koin) { modules(testModule) }                configureSerialization()                configureAuthentication()                configureRouting()            }
            val token = generateTestJWT(userId = "test-user")
            val client = createClient {                install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) { json() }            }
            val response = client.post("/users") {                contentType(ContentType.Application.Json)                bearerAuth(token)                setBody(CreateUserRequest("Alice", "[email protected]"))            }
            response.status shouldBe HttpStatusCode.Created        }    }})

設定

application.yaml

yaml
ktor:  application:    modules:      - com.example.ApplicationKt.module  deployment:    port: 8080
jwt:  secret: ${JWT_SECRET}  issuer: "https://example.com"  audience: "https://example.com/api"  realm: "example"
database:  url: ${DATABASE_URL}  driver: "org.postgresql.Driver"  maxPoolSize: 10

設定の読み取り

kotlin
fun Application.configureDI() {    val dbUrl = environment.config.property("database.url").getString()    val dbDriver = environment.config.property("database.driver").getString()    val maxPoolSize = environment.config.property("database.maxPoolSize").getString().toInt()
    install(Koin) {        modules(module {            single { DatabaseConfig(dbUrl, dbDriver, maxPoolSize) }            single { DatabaseFactory.create(get()) }        })    }}

クイックリファレンス: Ktor パターン

パターン説明
route("/path") { get { } }DSL を使用したルートグループ化
call.receive<T>()リクエストボディのデシリアライズ
call.respond(status, body)ステータス付きレスポンスの送信
call.parameters["id"]パスパラメーターの読み取り
call.request.queryParameters["q"]クエリパラメーターの読み取り
install(Plugin) { }プラグインのインストールと設定
authenticate("name") { }認証でルートを保護
by inject<T>()Koin 依存性注入
testApplication { }インテグレーションテスト

覚えておくこと: Ktor は Kotlin コルーチンと DSL を中心に設計されています。ルートをシンプルに保ち、ロジックはサービスに移し、依存性注入には Koin を使用してください。完全なインテグレーションカバレッジのために testApplication でテストしてください。

來源與署名

來源:affaan-m/ecc位於docs/ja-JP/skills/kotlin-ktor-patterns提交ef648e0

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架