Source profileQuality 78/100

affaan-m/ECC/docs/ja-JP/skills/kotlin-ktor-patterns/SKILL.md

kotlin-ktor-patterns

Review kotlin-ktor-patterns's use cases, installation, workflow, and original source instructions.

Source repository stars
234,327
Declared platforms
0
Static risk flags
1
Last source update
2026-07-27
Source checked
2026-07-28

Decision brief

What it does—and where it fits

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

Best for

    Not for

    • Tasks that require unconfirmed production actions or broad system permissions.
    • Environments where the pinned source and install steps cannot be inspected.

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorNot declaredNo explicit evidencePortability before use
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/affaan-m/ECC --skill "docs/ja-JP/skills/kotlin-ktor-patterns"
    Safe inspection promptEditorial

    Inspect the Agent Skill "kotlin-ktor-patterns" from https://github.com/affaan-m/ECC/blob/4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38/docs/ja-JP/skills/kotlin-ktor-patterns/SKILL.md at commit 4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

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

      Ktor HTTP サーバーの構築

      Ktor HTTP サーバーの構築Ktor プラグインの設定(Auth、CORS、ContentNegotiation、StatusPages)Ktor を使用した REST API の実装
    2. 02

      アプリケーション構造

      Review the “アプリケーション構造” section in the pinned source before continuing.

      Review and apply the “アプリケーション構造” source section.
    3. 03

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

      Review the “標準的な Ktor プロジェクトレイアウト” section in the pinned source before continuing.

      Review and apply the “標準的な Ktor プロジェクトレイアウト” source section.
    4. 04

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

      Review the “アプリケーションエントリーポイント” section in the pinned source before continuing.

      Review and apply the “アプリケーションエントリーポイント” source section.
    5. 05

      ルーティング DSL

      Review the “ルーティング DSL” section in the pinned source before continuing.

      Review and apply the “ルーティング DSL” source section.

    Permission review

    Static risk signals and limitations

    Network access

    medium · line 642

    The documentation includes network, browsing, or remote request actions.

    issuer: "https://example.com"

    Network access

    medium · line 643

    The documentation includes network, browsing, or remote request actions.

    audience: "https://example.com/api"

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score78/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars234,327SourceRepository attention, not individual Skill quality
    Compatibility0 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    affaan-m/ECC
    Skill path
    docs/ja-JP/skills/kotlin-ktor-patterns/SKILL.md
    Commit
    4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38
    License
    MIT
    Collected
    2026-07-28
    Default branch
    main
    View the original SKILL.md

    Ktor サーバーパターン

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

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

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

    アプリケーション構造

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

    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
    

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

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

    ルーティング DSL

    基本ルート

    // plugins/Routing.kt
    fun Application.configureRouting() {
        routing {
            userRoutes()
            authRoutes()
            healthRoutes()
        }
    }
    
    // routes/UserRoutes.kt
    fun 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)
            }
        }
    }
    

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

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

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

    kotlinx.serialization セットアップ

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

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

    @Serializable
    data class UserResponse(
        val id: String,
        val name: String,
        val email: String,
        val role: Role,
        @Serializable(with = InstantSerializer::class)
        val createdAt: Instant,
    )
    
    @Serializable
    data class CreateUserRequest(
        val name: String,
        val email: String,
        val role: Role = Role.USER,
    )
    
    @Serializable
    data 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)
        }
    }
    
    @Serializable
    data class PaginatedResponse<T>(
        val data: List<T>,
        val total: Long,
        val page: Int,
        val limit: Int,
    )
    

    カスタムシリアライザー

    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 認証

    // plugins/Authentication.kt
    fun 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")
    

    認証ルート

    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(エラーハンドリング)

    // plugins/StatusPages.kt
    fun 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 設定

    // plugins/CORS.kt
    fun 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 依存性注入

    モジュール定義

    // di/AppModule.kt
    val 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 の使用

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

    テスト用の Koin

    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
            }
        }
    }
    

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

    // ルートでリクエストデータを検証
    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

    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 テスト

    基本的なルートテスト

    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", "alice@example.com"))
                }
    
                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
            }
        }
    })
    

    認証ルートのテスト

    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", "alice@example.com"))
                }
    
                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", "alice@example.com"))
                }
    
                response.status shouldBe HttpStatusCode.Created
            }
        }
    })
    

    設定

    application.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
    

    設定の読み取り

    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 でテストしてください。

    Alternatives

    Compare before choosing