【Ktor】Getting Startedをやってみた② APIの作成

# はじめに
[前回の記事](https://qiita.com/rendaman0215/private/08f5d0194d22b9900984)に引き続き、Ktorのチュートリアル（Getting Started）を日本語訳しつつ進めていきます。

# 動作環境（再掲）
OS: MacOS Ventura
CPU: Apple M1 Pro
IDE: IntelliJ IDEA Community
Kotlin: 2.1.3

# HTTP APIの作成（Creating HTTP APIs）
今回のチュートリアルでは、モバイル、ウェブ、デスクトップ、B2Bサービスなどのアプリケーションのバックエンドとして機能するHTTP APIを作成します。
`ルート`がどのように定義され構造化されるか、`シリアライゼーションプラグイン`がどのように面倒な作業を簡略化するか、そしてどのようにアプリケーションの一部を手動と自動の両方でテストできるかについて見ていきます。

このチュートリアルでは、シンプルな JSON APIを構築して、架空のビジネスの顧客に関する情報、および処理したい注文を照会できるようにします。
システム内のすべての顧客と注文を一覧表示する機能を作成し、顧客と注文の情報を取得し、新しいエントリを追加したり古いエントリを削除したりする機能を提供します。

## Ktorプロジェクトの新規作成（Creating a new Ktor project）
例によって、無課金のため[Webベースのプロジェクトジェネレーター](https://start.ktor.io/?_ga=2.83819934.943213343.1667480869-2018674206.1667480869&_gl=1*1p8rvx4*_ga*MjAxODY3NDIwNi4xNjY3NDgwODY5*_ga_9J976DJZ68*MTY2NzQ4ODk1My4yLjAuMTY2NzQ4ODk1My42MC4wLjA.#/settings?name=tutorial-http-api&website=example.com&artifact=com.example.tutorial-http-api&kotlinVersion=1.7.20&ktorVersion=2.1.3&buildSystem=GRADLE_KTS&engine=NETTY&configurationIn=HOCON&addSampleCode=true&plugins=)を使用してプロジェクトを作成します。
今回は、以下の設定で作成していきます。
プラグインは、`ContentNegotiation` `kotlinx.seralization` `Routing`を使用します。

![スクリーンショット 2022-11-04 0.24.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264373/04d57c03-0a2c-0c3b-5009-595e897be162.png)
![スクリーンショット 2022-11-04 0.25.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264373/84e99cd5-d901-99d1-c6c0-8a8ad141f4e8.png)


## プロジェクトを調べる（Examine the project﻿）
上記でジェネレートしたプロジェクトを開くと以下のようなディレクトリ構成になっています。
![スクリーンショット 2022-11-04 0.32.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264373/1cfd8d08-7d2e-d21c-ae60-46d1f5b7fedc.png)

各ファイルの解説
- build.gradle.ktsファイル: Ktorサーバーとプラグインに必要な依存関係が含まれている
- main/resourcesフォルダ: 設定ファイルが含まれている
- main/kotlinフォルダ: 生成されたソースコードが格納されている

### 依存関係
`build.gradle.kts`に依存関係が記載されています。
```kotlin
dependencies {
    implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version")
    implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
    implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version")
    implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
    implementation("ch.qos.logback:logback-classic:$logback_version")
    testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}
```
- `ktor-server-core`： Ktor のコアコンポーネント
- `ktor-server-netty`： Nettyエンジン（非同期通信を行うアプリケーションを開発するためのフレームワーク）を追加し、サーバ機能を使用できるようにします
- `ktor-server-content-negotiation` `ktor-serialization-kotlinx-json`： JSONシリアライザ
- `logback-classic`： ログフォーマッター
- `ktor-server-test-host` `kotlin-test-junit`： テストツール

### 設定（application.conf と logback.xml﻿）
`application.conf` は、HOCONフォーマットの設定ファイルです。
Ktorではこのファイルを使ってリッスンするポートを決定したり、アプリケーションのエントリポイントを定義しています。

`logback.xml`はログ構造を定義しています。

## 顧客ルート（Customer routes﻿）
顧客に関する処理を実装していきます。
まず顧客に関連するデータを定義するモデルを作成する必要があります。
また、顧客を追加、リスト、および削除できるようにするための一連のエンドポイントを作成する必要があります。

### 顧客モデルを作成する
今回のケースでは、顧客は、`ID`、`姓`、`名`、および`メールアドレス`の情報を持っている必要があります。
Kotlinではデータクラスを使用して、これを簡単にモデル化します。
1. `models`という新しいパッケージを`com.example`の中に作成します。
2. modelsパッケージ内に`Customer.kt`を作成し、以下のコードを追加します。
```kotlin
package com.example.models

import kotlinx.serialization.Serializable

@Serializable
data class Customer(val id: String, val firstName: String, val lastName: String, val email: String)
```
※ APIレスポンスをJSONにシリアライズするため、kotlinx.serializationの@Serializableアノテーションを使用しています

### 顧客ストレージを作成する
実装のアプリケーションではDBを使いますが、このチュートリアルでは、イン・メモリ・ストレージを使用します。
`Customer.kt`ファイルのデータ・クラス宣言のすぐ後に、次の行を追加します。
```kotlin
val customerStorage = mutableListOf<Customer>()
```
これでCustomerクラスが定義され、顧客オブジェクトのストレージができたので、次はエンドポイントを作成してAPIで公開します。

### 顧客に関するルーティングを定義する
`/customer`エンドポイントへの`GET`、`POST`、`DELETE`リクエストに応答するようにします。
1. `com.example`に`routes`というパッケージを作成する
1. `routes`に以下のような`CustomerRoutes.kt`というファイルを作成する。
```kotlin
package com.example.routes

import io.ktor.server.routing.*

fun Route.customerRouting() {
    route("/customer") {
        get {

        }
        get("{id?}") {

        }
        post {

        }
        delete("{id?}") {

        }
    }
}
```

#### 顧客一覧
`call.respond`関数を使って`customerStorage`リストを返します。
この関数は、KotlinオブジェクトをJSONへシリアライズします。
```kotlin
package com.example.routes

import com.example.models.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Route.customerRouting() {
    route("/customer") {
        get {
            if (customerStorage.isNotEmpty()) {
                call.respond(customerStorage)
            } else {
                call.respondText("No customers found", status = HttpStatusCode.OK)
            }
        }
    }
}
```

#### 特定の顧客情報
以下のような実装になります。
`{id?}`を使うことで、任意のIDに対して検索をかけます。
```kotlin
get("{id?}") {
    val id = call.parameters["id"] ?: return@get call.respondText(
        "Missing id",
        status = HttpStatusCode.BadRequest
    )
    val customer =
        customerStorage.find { it.id == id } ?: return@get call.respondText(
            "No customer with id $id",
            status = HttpStatusCode.NotFound
        )
    call.respond(customer)
}
```


#### 顧客作成
```kotlin
post {
    val customer = call.receive<Customer>()
    customerStorage.add(customer)
    call.respondText("Customer stored correctly", status = HttpStatusCode.Created)
}
```

#### 顧客削除
```kotlin
delete("{id?}") {
    val id = call.parameters["id"] ?: return@delete call.respond(HttpStatusCode.BadRequest)
    if (customerStorage.removeIf { it.id == id }) {
        call.respondText("Customer removed correctly", status = HttpStatusCode.Accepted)
    } else {
        call.respondText("Not Found", status = HttpStatusCode.NotFound)
    }
}
```

### 顧客に関するルーティングを登録する
`plugins/Routing.kt`を以下のように修正するとルーティング登録されます。
```kotlin
package com.example.plugins

import com.example.routes.*
import io.ktor.server.application.*
import io.ktor.server.routing.*

fun Application.configureRouting() {
    routing {
        customerRouting()
    }
}
```

## 注文ルート（Order routes﻿）
次に注文に関する処理を実装していきます。
まず注文に関連するデータを定義するモデルを作成する必要があります。
また、注文を追加、リスト、および削除できるようにするための一連のエンドポイントを作成する必要があります。

### 注文モデルを作成する
`models`パッケージに`Order.kt`を新規作成します。
```kotlin
package com.example.models

import kotlinx.serialization.Serializable

@Serializable
data class Order(val number: String, val contents: List<OrderItem>)

@Serializable
data class OrderItem(val item: String, val amount: Int, val price: Double)
```
注文モデルは、顧客モデルより少し複雑で以下の二つのデータクラスからなっていますね。
- 何を何個いくらで買ったかという情報をもつ`OrderItem`データクラス
- 注文番号と`OrderItem`のリストからなる`Order`データクラス

### 注文に関するルーティングを定義する
#### 注文の一覧と特定の注文情報
```kotlin
package com.example.routes

import com.example.models.*
import io.ktor.server.application.*
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Route.listOrdersRoute() {
    get("/order") {
        if (orderStorage.isNotEmpty()) {
            call.respond(orderStorage)
        }
    }
}

fun Route.getOrderRoute() {
    get("/order/{id?}") {
        val id = call.parameters["id"] ?: return@get call.respondText("Bad Request", status = HttpStatusCode.BadRequest)
        val order = orderStorage.find { it.number == id } ?: return@get call.respondText(
            "Not Found",
            status = HttpStatusCode.NotFound
        )
        call.respond(order)
    }
}
```

#### 合計を表示する
```kotlin
fun Route.totalizeOrderRoute() {
    get("/order/{id?}/total") {
        val id = call.parameters["id"] ?: return@get call.respondText("Bad Request", status = HttpStatusCode.BadRequest)
        val order = orderStorage.find { it.number == id } ?: return@get call.respondText(
            "Not Found",
            status = HttpStatusCode.NotFound
        )
        val total = order.contents.sumOf { it.price * it.amount }
        call.respond(total)
    }
}
```

### 注文に関するルーティングを登録する
最後に顧客モデルの時と同様に`plugins/Routing.kt`にルーティングを登録して完了です！
```kotlin
package com.example.plugins

import com.example.routes.*
import io.ktor.server.application.*
import io.ktor.server.routing.*

fun Application.configureRouting() {
    routing {
        customerRouting()
        listOrdersRoute()
        getOrderRoute()
        totalizeOrderRoute()
    }
}
```

# まとめ
今回は、Ktorを用いてAPIサーバを作成しました。
Ktorには、ルーティグやシリアライズといった便利プラグインが多くて開発しやすいですね。
一方で、そういったプラグインの使い方の学習コストも高いと思われるので、よく使うプラグインのまとめなんかもおいおい作っていきたいと思います。