1
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

Organization

KtorでAPIを作成しました。

この記事は ハンズラボ Advent Calendar 2018 19日目の記事です。

おはようございます。@naokiurです。
今年2回目です。
よろしくお願い致します。

前回、Ktorのテストメソッドを通して、
Kotlinの言語を、少し学ぶことができました。
今回は、KtorでWeb APIを作成していきたいと思います。

環境

  • MacBook Pro (Retina 13-inch、Early 2015)
  • macOS High Sierra 10.13.6
  • Java 1.8.0_152
  • IntelliJ IDEA CE 2018.2.5

実施したこと

  • APIのテストクラスを作成する
    • GETメソッド
    • POSTメソッド
  • APIを作成する
    • GETメソッド
    • POSTメソッド

サンプル

分かりやすい例で、
以下のような ユーザーを管理するAPIを作成したいと思います。

  • GETメソッドのAPIで、存在するユーザーの一覧を取得する show
  • POSTメソッドのAPIで、ユーザーを追加する create

ユーザーは、すごくシンプルに、以下の属性を持つようにします。

  • ユーザーID
  • ユーザー名
  • フルネーム

結果

APIのテストクラスを作成する

まず前回学んだ、テストクラスから作成したいと思います。

showメソッドのテストメソッド

    @Test
    fun testShow() = withTestApplication(Application::api) {

        handleRequest(HttpMethod.Get, "/show").run {

            assertEquals(HttpStatusCode.OK, response.status())
            println(response.content)
        }
    }

レスポンスの中身を確認して、期待値と等しいかを確認したほうが良いと思いますが、
今は、シンプルにレスポンスの中身を出力する程度に留めます。

前回学んだように、
.run {}は、 TestApplicationCallに対して実行しているため、
TestApplicationCallのフィールドである、
responseにアクセスすることができます。

responseには、 Application::apiの実行結果である以下を参照することができます。

  • status()
  • content

それらを確認し、テストメソッドの判定基準としております。

createメソッドのテストメソッド

    @Test
    fun testCreate() = withTestApplication(Application::api) {

        val gson = GsonBuilder().setPrettyPrinting().create()
        val param = User(UserId(3), UserName("test"), FullName("test", "hoge"))

        handleRequest(HttpMethod.Post, "/create") {

            addHeader(HttpHeaders.Accept, ContentType.Text.Plain.toString())
            addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
            setBody(gson.toJson(param))

        }.run {

            assertEquals(HttpStatusCode.OK, response.status())
            assertEquals("Success", response.content)
        }
    }

JSON形式をPOSTするテストメソッドです。

gsonを用いて、
UserクラスをJSON形式に変換し、パラメータとしています。

class User constructor(
    val userId: UserId,
    var userName: UserName,
    var fullName: FullName) {

}

class UserId(val id: Int) {
    fun equals(userId: UserId): Boolean {
        return id == userId.id
    }
}
class FullName constructor(val firstName: String, val lastName: String) {
    val name = "$firstName $lastName"
}
class UserName constructor(val name: String) {
}

この時点では、
テストクラス以外を作成していないので、
コンパイルエラーです。

スクリーンショット 2018-12-18 8.47.46.png

APIを作成する

ここで、ようやくKtorにおけるAPIの作成です。

Ktorは、
とてもシンプルにルーティングを定義することができる
と思っています。

fun Application.api() {

    routing {
        get("/show") {
        }
        post("/create") {
        }
    }
}

ルーティング自体はこれだけです。

Applicationの拡張関数を作成し、
routing内に、各リクエストのハンドリングを記載する形です。

このままですと、
何も処理していないため、当然ながらテストが失敗となってしまいます。

スクリーンショット 2018-12-18 9.20.27.png

APIサーバを起動する

この時点で、APIサーバとして起動することができます。

このままだと、何も返却しておらず、ブラウザ上で確認できないので、
レスポンスを作成します。

レスポンスを作成する

APIサーバが起動されたか確認したいため、
シンプルなレスポンスを記載します。

fun Application.api() {

    routing {
        get("/show") {
            call.respond("show!")
        }
        post("/create") {
            call.respond("create!")
        }
    }
}

レスポンスは、call.respondに格納することで、
指定することができます。

実は… call.respond(message: Any)を実行すると、
レスポンスのHTTPステータスが 200になるので、
この時点で、上記テストは通ってしまいます :innocent:

テストコードは、もっと適切に書いていきたいと思います :innocent: ent:

APIサーバを実行する

IntelliJの Runで、
MainClassに io.ktor.server.netty.EngineMainを指定してあげることで、
実行することができます。

スクリーンショット 2018-12-18 9.58.53.png

GETメソッドであるshow

テストコードは通ってしまいましたが、
JSONを返却するところまで実装したいと思います。

fun Application.api() {
    install(ContentNegotiation) {
        gson {
            setDateFormat(DateFormat.LONG)
            setPrettyPrinting()
        }
    }

    routing {
        get("/show") {
            val testUser = User(UserId(1), UserName("test1"), FullName("firstTest1", "lastTest1"))
            call.respond(testUser)
        }
        post("/create") {
            call.respond("create!")
        }
    }
}

JSONで返却するために、 install関数を用いました。
Ktorでは、これを用いることで、
リクエストやレスポンスに機能的な処理を注入することができます。
(routingも、installにより注入された処理の一つです。)

その中のContentNegotiationは、
HTTPヘッダの Content-TypeAcceptを自動変換してくれる機能で、
そこに gsonを指定することで、
JSON形式に自動変換してくれます。

スクリーンショット 2018-12-19 1.37.50.png

複数APIを作成し、レスポンスを返却する際、
何度も変換処理を書かなくて済みますね。

POSTメソッドであるcreate

リクエストを受け取って、
登録する処理を作成します。

DB接続は別の回に持ち越しさせて頂き、
ここではリクエストの内容を出力、テストコードが通るところまで実装します。

fun Application.api() {
    install(ContentNegotiation) {
        gson {
            setDateFormat(DateFormat.LONG)
            setPrettyPrinting()
        }
    }

    routing {
        get("/show") {
            val testUser = User(UserId(1), UserName("test1"), FullName("firstTest1", "lastTest1"))
            call.respond(testUser)
        }
        post("/create") {
            val parameter = call.receive<User>()

            println(parameter.userId.id)
            println(parameter.userName.name)
            println(parameter.fullName.name)

            call.respond("Success")
        }
    }
}

call.receive<User>()で、リクエストを取得します。
テストコード上でJSON形式でリクエストを送信しており、
そのJSON構造とマッチするクラスを指定してあげると、
自動的にマッピングしてくれます。

スクリーンショット 2018-12-19 1.38.08.png

受け取ったリクエストを、
そのまま別のビジネスロジックに渡すことができそうで、
便利ですね。

おわりに

駆け足になってしまいましたが、
GET/POSTのAPIを作成することができました。

今後はDB接続、
ログ出力、エラーハンドリングなどの構築方法という、アプリケーション基盤、
多量のAPIを作成した際のクラス設計など、
習得していきたいと思います。

ハンズラボ Advent Calendar 2018
20日目は、@yktakaha4 さんです!

参考にさせて頂きました

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
1
Help us understand the problem. What are the problem?