虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Kotlinで軽量なREST APIサーバーを作ろう 〜 KtorとExposedことはじめ 〜

はじめまして、こんにちは。虎の穴ラボに最近ジョインしました鷺山と申します。
現在はとらのあな通販の開発チーム (通販チーム) に参加しています。

通販チームでは入社時の研修として、日々の業務でも活用できる社内向けのツールをKotlinを使って開発することになっています。私は入社する以前からKotlinのことは気になっていたので、研修の機会にKotlinに本格的に触れることができたのはラッキーでした。また実際にKotlinでプログラムを書いてみると、スッキリと簡潔に書けるところが気に入っています。

今回の研修では、Kotlinを使って簡単なREST APIサーバーを作りました。1から調べながらの開発でしたが、Web上には開発のためのまとまった情報がまだそこまで多くない印象です。

そこで、本エントリーではKotlinによるREST APIサーバー開発がすぐに始められるスターター的なコード一式を作ってみたので紹介したいと思います。

ソースコード

本エントリーのソースコードは以下にあります。

Dockerfileおよびdocker-compose.ymlも含まれておりますので、Dockerが使える環境があれば手元で動かしてお試しいただけます。
(Windowsで動かす場合は、gradlewの改行コードがLFになるようにしてください。)

プログラムの構成

本エントリーでは、REST APIを介してデータベース中のmessagesテーブルのmessage属性を読み書きする単純なプログラムを扱います。
WebアプリケーションフレームワークにはKtor、SQLライブラリにはExposedというライブラリを使います(それぞれ後述します)。
データベースにはMySQLを使います。

f:id:toranoana-lab:20210512152532p:plain

実行環境

  • Kotlin: 1.4.32
  • Ktor: 1.5.4
  • Exposed: 0.31.1
  • JDK: 1.8
  • MySQL: 5.7
  • (Docker: 20.10.6 …… Dockerを使わない場合は不要)
  • (Docker Compose: 1.29.1 …… Dockerを使わない場合は不要)
  • macOS Catalina および Windows 10

Ktorとは?

Ktorは、Kotlinの開発元のJetBrains社による、Kotlin製のWebアプリケーションフレームワークです。
軽量で起動が早いので、デバッグもしやすく、研修中も快適に開発ができました。
シンプルなので学習コストも低く、とっつきやすい点も気に入っています。

新規にKtorプロジェクトを作成する際には、Ktor公式が提供している以下のジェネレータを利用するのが便利でおすすめです。

このジェネレータでは、Ktorプロジェクトに導入するフレームワークやライブラリを目的に合わせてピックアップすることができます。
本エントリーでは、以下のフレームワークやライブラリを選択しています:

  • プロジェクト: Gradle
  • サーバーエンジン: Jetty
  • Ktorバージョン: 1.5.4
  • 他にも、サーバー側の機能 (Server Features) から CORS, GSON を選択して導入しています。

導入するフレームワークやライブラリの選択が完了したら、左下の "Build" ボタンを押すことで、選択したライブラリを含むKtorプロジェクトのソースコード一式をZIP形式でダウンロードすることができます。
今回はデータベースにも接続するので、このプロジェクトにMySQL用のJDBCドライバと、後述するSQLライブラリのExposedも導入します。

具体的にはgradle.propertiesbuild.gradleに以下を追記します。

exposed_version=0.31.1
mysql_connector_version=8.0.25
dependencies {
    ...
    implementation "org.jetbrains.exposed:exposed-core:$exposed_version"
    implementation "org.jetbrains.exposed:exposed-dao:$exposed_version"
    implementation "org.jetbrains.exposed:exposed-jdbc:$exposed_version"
    implementation "mysql:mysql-connector-java:$mysql_connector_version"
}

Ktorでエンドポイントを設定

前述のmessagesテーブルのレコードを読み書きするためのREST APIのエンドポイントを作成します。
今回は以下の2つのURIでリクエストを受け付けるものとします。

  • POST /api/v1/messages …… メッセージを投稿するためのエンドポイント
  • GET /api/v1/messages/{id} …… メッセージを取得するためのエンドポイント

まずmessagesリソースを扱うMessagesRoutes.ktファイルを作成し、このファイルにそれぞれのエンドポイントへのルーティングと、それに対する処理を記述します。

fun Route.messagesRoutes(messageDataAccessor: MessageDataAccessor) {
    route("/api/v1/messages") {
        // メッセージ投稿用のエンドポイント
        post {
            val message = call.receive<Message>()
            messageDataAccessor.addMessage(message)
            call.respond(HttpStatusCode.OK)
        }

        // メッセージ取得用のエンドポイント
        get("/{id}") {
            val messageId = call.parameters["id"] ?: ""
            val result = messageDataAccessor.getMessage(messageId)
            if (result != null) {
                call.respond(HttpStatusCode.OK, result)
            } else {
                call.respond(HttpStatusCode.NotFound)
            }
        }
    }
}
  • 上記のmessageDataAccessorはデータベースアクセスを扱うMessageDataAccessorクラスのインスタンスです。次のセクションで解説します。
  • post { ... } にメッセージの投稿処理を記述しています。
    • リクエストボディをMessage型で受け取り、messageDataAccessor.addMessage(message)でその内容をデータベースに登録します。最後にOKレスポンスを返します。
  • get("/{id}") { ... } にメッセージの取得処理を記述しています。
    • メッセージIDをパスパラメータidから受け取り、messageDataAccessor.getMessage(messageId)でそのIDに該当するメッセージをデータベースから取得します。
    • データベースに該当のIDのメッセージが存在する場合はそれをレスポンスとして返します。存在しない場合はステータスコード404 (Not Found)を返します。

上記のルートmessagesRoutesをアプリケーションのエントリーポイントApplication.ktのルーティングに追加します。

fun Application.module() {
    ...
    routing {
        messagesRoutes(MessageDataAccessor()) // 追加
    }
}

以上でエンドポイントの設定が完了しました。
次に、Exposedによるデータアクセス処理を記述していきます。

Exposedとは?

Exposedは、同じくJetBrains社によるKotlin製のSQLライブラリです。こちらもKtorと同様、軽量かつシンプルで扱いやすいライブラリとなっています。

Exposedは、データベースアクセスの記述方法としてDSL方式DAO方式の二種類が用意されています。本エントリーでは、より柔軟な表現が可能な前者のDSL方式で記述しています。

Exposedによるデータアクセス

今回扱うmessagesテーブルは、DDLでは以下のように定義されるものとします。

CREATE TABLE messages (
    id VARCHAR(36) NOT NULL,
    message TEXT NOT NULL,
    PRIMARY KEY (id)
);

まず、上記のテーブルを表現するMessageTable.ktを作成します。

object MessageTable : Table("messages") {
    val id = text("id")
    val message = text("message")

    override val primaryKey = PrimaryKey(id)
}

次に、上記のテーブルを読み書きするための処理をMessageDataAccessor.ktに記述します。

class MessageDataAccessor {
    // メッセージを追加する
    fun addMessage(message: Message) {
        transaction {
            MessageTable.insert {
                it[this.id] = message.id
                it[this.message] = message.message
            }
        }
    }

    // メッセージを取得する
    fun getMessage(id: String): Message? {
        var result: Message? = null
        transaction {
            result = MessageTable
                .select { MessageTable.id eq id }
                .map { convertToMessage(it) }
                .firstOrNull()
        }
        return result
    }

    // messagesテーブルのレコードをMessageオブジェクトに変換する
    private fun convertToMessage(row: ResultRow): Message {
        return Message(
            id = row[MessageTable.id],
            message = row[MessageTable.message],
        )
    }
}

最後に、データベースの接続情報と接続処理をApplication.ktに追記します。

fun Application.module() {
    ...
    // データベースのホスト名 (環境変数 `DATABASE_HOSTNAME` から取得)
    val databaseHostName = System.getenv("DATABASE_HOSTNAME") ?: "localhost"

    Database.connect(
        url = "jdbc:mysql://$databaseHostName:3306/ktor-starter",
        driver = "com.mysql.cj.jdbc.Driver",
        user = "root",
    )
}

起動と動作確認

これまで作成したプログラムの起動と動作確認を行います。
今回はデータベースにMySQLを使います。ローカル環境にMySQLを直接インストールしてもよいですが、本エントリーではDockerとDocker Composeを使って確認を行います。

具体的なDockerfileおよびdocker-compose.ymlの内容は以下をご参照ください。

messagesテーブルの作成

本エントリーのプログラムを実行するには、事前にデータベースにmessagesテーブルを作成しておく必要があります。
MySQLコンテナには、初期化時にコンテナ内の特定のディレクトリ下にあるSQLファイルを自動的に実行する仕組みが用意されており、今回はこれを利用しています。ローカル側のDDLファイルが配置されているディレクトリ (./database/init_sqls) にリモート側の該当ディレクトリ (/docker-entrypoint-initdb.d) をマッピングすることで (./database/init_sqls:/docker-entrypoint-initdb.d)、MySQLコンテナの起動時に自動的にmessagesテーブルが作成されるようにしています。

Dockerコンテナ群の起動

以下のコマンドでDockerコンテナ群を起動します。
(Windowsの場合は、gradlewの改行コードがLFになるようにしてください。)

docker-compose up -d

APIコンテナktor-starter_api_1とデータベースコンテナktor-starter_database_1が起動することを確認します。

REST APIの実行

まず、メッセージ投稿API (POST /api/v1/message) を実行します。

# macOSやLinuxの場合
curl http://localhost:8080/api/v1/messages -d '{ "id": "11111111-1111-1111-1111-111111111111", "message": "Hello!" }' -H 'Content-Type: application/json' -v
# Windows (PowerShell) の場合
curl -Method Post http://localhost:8080/api/v1/messages -Body '{ "id": "11111111-1111-1111-1111-111111111111", "message": "Hello!" }' -Headers @{ 'Content-Type' = 'application/json' }

200 OKが返ってくることを確認します。

次に、メッセージ取得API (GET /api/v1/messages/{id}) を実行します。

# macOS、Linux、 Windows (PowerShell) 共通
curl http://localhost:8080/api/v1/messages/11111111-1111-1111-1111-111111111111

POSTで投稿したメッセージが返ってくることを確認します。

{"id":"11111111-1111-1111-1111-111111111111","message":"Hello!"}

まとめ

本エントリーでは、データベースに接続するREST APIサーバーをKtorExposedというKotlin製フレームワークの組み合わせで作りました。どちらも軽量かつシンプルで扱いやすいのでおすすめです。
本エントリーのプログラムをもとにKotlinでのアプリケーション開発を始めてみようと思った方がいれば幸いです。

P.S.

■ 直近のイベント情報

とらのあなエンジニア&マーケター採用説明会+リモート懇親会【地方勤務も可能!】
を5月26日 (水) 19:30から開催します。YouTubeでのライブ配信となりますので、お気軽にご参加ください!
yumenosora.connpass.com

【オンライン】オタクが最新技術を追うLTイベント#24【初心者歓迎】【テーマフリー】
を5月28日 (金) 19:30から開催します。こちらは発表者も募集中ですので、LT初心者という方でもぜひご応募ください!
yumenosora.connpass.com

■ 採用情報

とらのあなでは、オタクなエンジニアを募集しています。
yumenosora.co.jp

カジュアル面談も随時開催中です。お申し込みはこちら!
yumenosora.connpass.com

■ ToraLab.fmスタートしました!

メンバーによるPodcastを配信中!是非スキマ時間に聞いて頂けると嬉しいです。
anchor.fm

■ Twitterもフォローしてくださいね!

ツイッターでも随時情報発信をしています。
twitter.com