TECH MEMO / FIELD NOTEKtor入門。Kotlinで小さなWeb APIを作って実感した魅力と学び
はじめに
KotlinのWebフレームワークであるKtor(ケイター)に触れる機会がありました。普段はインフラやSRE領域を担当することが多く、Web APIやマイクロサービスをKotlinで作る場合の使い勝手に興味があったため、実際に小さなアプリケーションを作ってみました。
KtorでWeb APIとBFF(Backend for Frontend)を構築し、Angularのフロントエンドから利用できるところまで実装しました。この記事では、Ktorの構成、JetBrains Junieを使った開発、Spring Bootとの違い、運用を考えるうえで気づいた点を紹介します。
作成したアプリケーションの全体像
バックエンドとフロントエンドは別のリポジトリに分けました。作成時の主なバージョンは次のとおりです。
- バックエンド: Ktor 3.1.0(Kotlin 2.4.0、JDK 21、Exposed 0.61.0)
- フロントエンド: Angular 22.1.0(Signals、Standalone Components)
システムアーキテクチャ
バックエンドは、画面向けのBFFと、DBにアクセスする内部API(以下、Core API)に分けました。
セットアップ
ここではKotlinとPostgreSQLのセットアップは省略し、今回試したJunie Localの導入手順だけを記載します。
IntelliJ IDEA × Junie (Junie Local) セットアップ手順
今回は、JetBrainsのJunie Localも試しました。Qwen3.6-27B-4bitを手元のMacで動かすため、モデルのダウンロード後は、プロンプトやソースコードをクラウドの推論サービスへ送らずに利用できます。サブスクリプションやAPIクレジットも不要です。
1. インストール(公式ワンライナー)
ターミナルを開き、公式のインストールスクリプトを実行します。
curl -fsSL https://junie.jetbrains.com/install.sh | bash -s -- --local-model
※ すでに Junie CLI をインストール済みの場合は、Junie のプロンプト上で以下を入力するだけです。
/local
2. モデルのダウンロードと起動
- モデルのダウンロード
- コマンドを実行すると、初回に約20GBのモデルデータがダウンロードされます。
- ローカル推論サーバーの自動起動
- ダウンロードが完了すると、手元のマシン内でローカル推論サーバーが自動で立ち上がります。
- 作業開始
- 通常は設定ファイルやエンドポイントを手動で指定する必要はありません。
- クラウドモデルへ戻す場合は、
/modelコマンドで切り替えます。
3. IntelliJ IDEAとの連携
- IntelliJ IDEA側にJunieプラグインを導入し、CLIとIDEで同じプロジェクトを開くと、Junie CLIが対応するIDEを自動検出します。
- 接続中は、IDEのインデックスを使ったシンボル検索、コード検査、テスト実行などを利用できます。接続状態は
/ideコマンドで確認できます。
Junie Local を実際に使ってみた使用感
実際にローカル環境で Junie Local を動かしながら開発を進めてみました。
タスクによってはスムーズにパッとコードを提案・生成してくれる場面もありましたが、「思考中(Thinking…)」の時間がかなり長く続いて待たされるケースのほうが圧倒的に多いというのが率直な感想です。
推論や思考のスピードにムラがあり、待たされる時間のほうが長いため、開発のリズムが崩れてしまいやすく、現時点では「日々の業務や実戦でバリバリ使う」にはまだ少し厳しい印象でした。
外部にコードを一切送信できないセキュアな環境での選択肢としては非常に夢がある技術ですが、日常の開発で快適に使えるようになるには、今後のモデル軽量化やローカル推論のさらなる最適化に期待したいところです。
Ktor バックエンド実装のポイント
1. 1プロセスで2つのサーバー(BFF + API)を起動する
学習用の構成では、単一のKotlinプロセスから2つのNettyサーバーを起動しました。
// main.kt
fun main() {
val apiPort = System.getenv("API_PORT")?.toIntOrNull() ?: 8081
val bffPort = System.getenv("BFF_PORT")?.toIntOrNull() ?: 8080
val apiServer = embeddedServer(
factory = Netty,
port = apiPort,
host = "127.0.0.1",
module = Application::apiModule
)
val bffServer = embeddedServer(
factory = Netty,
port = bffPort,
host = "0.0.0.0",
module = Application::bffModule
)
apiServer.start(wait = false)
bffServer.start(wait = true)
}
start(wait = false)でCore APIを起動したあと、BFFをstart(wait = true)で起動し、メインスレッドを待機させています。ローカルでは扱いやすい一方、片方の停止がもう片方に影響し、個別にスケールすることもできません。本番環境で独立した可用性やスケーリングが必要なら、BFFとCore APIを別プロセスに分けるのが適切です。
2. Exposedによる型安全なDBアクセス
データベースアクセスには、JetBrains製のKotlin向けSQLライブラリであるExposedを利用しました。ExposedにはSQL DSLとDAOの2つのAPIがあり、ここではSQL DSLでテーブルを定義しています。
// db/entities/Users.kt
object Users : Table("users") {
val id = integer("id").autoIncrement()
val name = varchar("name", 100)
val email = varchar("email", 255).uniqueIndex()
val passwordHash = varchar("password_hash", 255)
override val primaryKey = PrimaryKey(id)
}
KotlinのDSLを使うため、コード上に定義したカラム名の誤りや型の不整合はコンパイル時に見つけやすくなります。ただし、実際のDBスキーマとの差分までコンパイラが検証するわけではありません。マイグレーションとテストによる確認は別途必要です。
3. JWT認証とRFC 9457形式のエラーレスポンス
認証にはBCryptによるパスワードのハッシュ化とJWT(JSON Web Token)を採用しました。APIのエラーレスポンスは、RFC 9457(Problem Details for HTTP APIs)の形式にそろえています。
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"detail": "このメールアドレスは既に登録されています。",
"instance": "/v1/api/auth/register"
}
JSON本体に加えて、レスポンスのContent-Typeにはapplication/problem+jsonを指定します。また、instanceは問題が発生した個別の事象を識別するURI参照です。単にリクエストパスを入れる設計もできますが、用途をAPI内で統一しておく必要があります。
実際に触って実感した「Kotlin / Ktor」の魅力と学び
KtorでWeb APIとBFFを作り、特に印象に残った点を4つ挙げます。
1. 設定と処理の流れをコードで追いやすい
Spring Bootでは、アノテーションや自動設定によって少ない記述で機能を組み込めます。一方、慣れないうちは、どの設定が適用されたかを追うのに時間がかかることもあります。
Ktorでは、プラグインの導入をinstall、ルーティングをroutingなどのKotlinコードで記述します。設定場所が明示されるため、今回の小規模なアプリケーションでは処理の流れを追いやすいと感じました。
2. 必要な機能だけを組み込みやすい
Ktorは必要なプラグインを選んで組み込む構成です。今回のアプリケーションでは起動が速く、修正と再起動を繰り返す作業を進めやすく感じました。
起動時間が短ければ、KubernetesでPodを増やしてからReadyになるまでの時間も短縮できます。ただし、実際の起動時間はDB接続、マイグレーション、外部サービスとの通信、JVMの設定などにも左右されます。HPAの追従性を評価する場合は、コンテナ起動からreadiness probeが成功するまでを実環境で計測する必要があります。
3. コルーチン(Coroutines)ネイティブな非同期処理
KtorはKotlinのコルーチンを前提に設計されています。BFFで複数の内部APIを呼び出す場合は、coroutineScope内でasyncを使うと、構造化された形で並行実行できます。
ただし、asyncを付ければすべての処理が高速になるわけではありません。互いに独立したI/O処理に使い、JDBCのようなブロッキング処理は適切なDispatcherへ切り替えるなど、利用するライブラリの特性に合わせる必要があります。
4. Kotlinの型システムがWeb開発を堅牢にする
- Null安全性: nullableな値を型で表し、必要な処理をコンパイラに確認してもらえます。
sealed interfaceとwhen式: 認証結果を成功、パスワード不一致、重複エラーなどの型で表すと、分岐の追加漏れを見つけやすくなります。- 拡張関数:
Application.apiModule()のように機能単位でモジュールを分けられます。
(おまけ)Angularフロントエンドとの連携
フロントエンドでは、Angular SignalsとStandalone Componentsを採用しました。
AuthServiceでログイン状態(currentUser、isLoggedIn)をsignal()とcomputed()で管理- HTTP Interceptorでリクエストヘッダーに
Authorization: Bearer \<JWT\>を付与 - Ktor側のCORSプラグインで
http://localhost:4200を許可
BFF経由でトークンを発行し、/auth/meでユーザー情報を取得して画面遷移するまでの認証フローを確認できました。
まとめ
実際に手を動かしてみると、Ktorは設定やルーティングをKotlinコードで記述でき、処理の流れを把握しやすいフレームワークでした。
- フレームワークの設定をKotlinコードで明示したい
- 必要な機能を選んでWeb APIやBFFを構成したい
- Kotlinの型システムやコルーチンを活用したい
このような要件には、Ktorが選択肢の一つになります。今後はtestApplicationを使ったテストの拡充と、Dockerコンテナ化にも取り組む予定です。
