Springboot Webflux + Kotlin + coroutine + Router FunctionsでOpenAPIサンプルを作ってみた

最近、サーバーサイドKotlinでノンブロッキングAPIを書く機会があり、今までは`Webflux` + `Kotlin` + `Reacotr`で書いていたのですが、`coroutine`を利用すると更に簡潔にかけるというウワサを知り、今後も同様のAPIを書く機会が増えそうなので、自身のテンプレートとして、`Springboot Webflux` + `Kotlin` + `coroutine`　+ `Router Functions`で`OpenAPI`サンプルを作ってみました。

# 前提条件
- Java 11
- Springboot Webflux 2.6.9
- Kotlin 1.6.21
- Docker 4.9.1
- macOS 12.4

コードベースは[Spring Initializr](https://start.spring.io/)から生成しました。

コードは[https://github.com/yasuflatland-lf/spring-boot-webflux-kotlin-coroutine](https://github.com/yasuflatland-lf/spring-boot-webflux-kotlin-coroutine)にあります。役に立ちそうであったらスターをつけてくれたら励みになります！

# ルーターとハンドラー
通常のMVCだと、`Model`、`Service`、`Controller`という感じでコントローラーをエントリポイントとしていたのを、`Router Function`では`Router`(もしくは`Configuration`)がコントローラー、`Handler`がそのビジネスロジックをまとめる、というような構成になります。

`Router`の中ではAPI設定を`RouterOperations`というアノテーションで作成し、ルーティングはドメイン言語で完結にかけるようになり、Nodejs系のExpressなどのような書き味に近く感じました。ルーテイングは例えば以下のような感じになります。

```kotlin
fun router(handler: TodoHandler) = coRouter {

    accept(APPLICATION_JSON).nest {
        GET("/todos/{id}", handler::findById)
        GET("/todos", handler::findAllByStatus)
    }
    POST("/todos", handler::edit)
    PATCH("/todos", handler::edit)
    DELETE("/todos/{id}", handler::delete)
}
```

# OpenAPIを生成するためのアノテーション
`build.gradle.kts`に以下のライブラリが必要です

```gradle:build.gradle.kts
// OpenAPI
implementation("org.springdoc:springdoc-openapi-kotlin:${property("openAPIVersion")}")
implementation("org.springdoc:springdoc-openapi-webflux-ui:${property("openAPIVersion")}")
```

合わせてルートクラスである`SpringBootWebfluxKotlinCoroutineApplication`に以下のアノテーションを設定します。
```kotlin:SpringBootWebfluxKotlinCoroutineApplication
@SpringBootApplication
@OpenAPIDefinition(
    info = io.swagger.v3.oas.annotations.info.Info(
        title = "Todo Service",
        version = "1.0.0",
        description = "Springboot Webflux and Coroutine example implementations"
    )
)
class SpringBootWebfluxKotlinCoroutineApplication
```

ルーターには以下のような`RouterOperations`,`RouterOperation`で定義を作っていきます。

```kotlin:TodoRoutes.kt
@Bean
@RouterOperations(
    RouterOperation(
        path = "/todos",
        method = [RequestMethod.GET],
        operation = Operation(
            operationId = "getTodoById",
            summary = "Find a todos by id",
            requestBody = RequestBody(
                content = [Content(schema = Schema(implementation = Todo::class))]
            ),
            responses = [
                ApiResponse(
                    responseCode = "200",
                    content = [Content(schema = Schema(implementation = Todo::class))]
                ),
                ApiResponse(
                    responseCode = "404",
                    description = "no Todo with such id found"
                )
            ]
        )
    ),
    ・
    ・
    ・
)
```

# テスト
考慮した点は以下になります。
- 色々テストフレームワークを見てみた結果、スター数、ドキュメントの充実さと、かゆいところに手がとどくアサーションの充実度から、[Kotest](https://kotest.io/)を採用しました。
- テストはなるべくモックは使わず、本物のデータベースに対してテストをしたかったので、[Testcontainers](https://www.testcontainers.org/)を採用、`MySQL`サーバーをテストで使うようにしました。
- テスト時のデータベース起動と初期化は`ContextConfiguration`アノテーションを活用し、抽象化クラス`AbstractContainerBaseTest`をテスト側で継承して共通化を図りました。

## リポジトリのテスト
Kotestの威力で繰り返しテストも以下のように簡潔に書けます。

```kotlin:TodoRepositoryTest.kt
test("findAllByStatusEquals failure pattern") {
    forAll(
        row(true, true, 1, true),
        row(false, false, 1, false),
    ) { orgStatus, filterStatus, amount, resultStats ->

        runTest {
            var todo = Todo(null)
            todo.task = "test"
            todo.status = orgStatus
            todoRepository.save(todo)

            var result = todoRepository.findAllByStatusEquals(filterStatus, PageRequest.of(0, 20))
            result.first().status shouldBe resultStats
            result.count() shouldBe amount
        }
    }
}
```

## ルーターのテスト
ルーターのテストは`WebTestClient`を活用して、`Fluid Style`で書けます。

```kotlin:TodoRoutesTest.kt
test("Get all todos no params Test") {
    var todo = arrayListOf<Todo>()

    WebTestClient
        .bindToServer()
        .baseUrl("http://localhost:$port")
        .build()
        .get()
        .uri("/todos")
        .accept(MediaType.APPLICATION_JSON)
        .exchange()
        .expectStatus()
        .is2xxSuccessful
        .expectBody<List<Todo>>()
        .isEqualTo(todo)
}
```

# まとめ
ちょうど初めて`Typescirpt`で`Nodejs`サーバーを書いた後だったので、`coroutine`と`Router Function`を利用すると、Kotlinが型がカッチリ定義されたTypescriptのような書き心地でAPIがかけるなぁと感じました。

また、Springbootはエンタープライズでの利用で鍛えられてきたフレームワークなので、情報も非常に多く、また作りも堅牢なので、そういったエコシステムの恩恵を受けつつ、KotlinはJavaより簡潔かつ見通しの良いコードが自然とかけてしまうなあ、という印象です。

習得にも時間がかからない、というかJavaっぽい書き方で最初は誤魔化しても、なんとかかけちゃうし、ちゃんとした書き方を覚えるとどんどん簡潔にかけるので、学ぶのが楽しい言語で、今回のサンプル作りで、Kotlin推しになってしまいました。