はじめに
前回の記事で、SpringBoot (Gradle) を使ったバックエンドと、Vue.js (Node.js) を使ったフロントエンドで構成される SPA の環境構築手順を紹介しました。
この構成でフロントエンドに TypeScript を導入しようと考えたのですが、SPA や TypeScript に不慣れな私は、「バックエンドとフロントエンドで別々に型定義を行うのは手間が増えるし、修正漏れが起こりそうだな」と思ってしまいました。
もちろん、小規模なプロジェクトであれば、手動で型定義を同期するコストはそれほど高くありませんし、バックエンドとフロントエンドが密に連携していれば、API の仕様変更にも追従しやすいです。
それでも、気になったことはやってみようの精神で調べてみた結果、OpenAPI にたどり着きました。
私が求めていたのは、以下の 2 点です。
1. フロントエンドからバックエンドの API に型安全に接続したい
2. API クライアントコードを自動生成したい
OpenAPI を導入すれば、バックエンドのコードから自動で OpenAPI 仕様書を生成し、その仕様書を元にフロントエンド用の API クライアントコードも自動生成できるようなので、要件が満たせそうです。
実際に試してみたところ、設定が少なく、知識がゼロの状態からでもスムーズに導入できたので、今回はこの導入手順を紹介しようと思います。
実用的でない部分もあるかもしれませんが、OpenAPI の仕組みを理解し、ツールに慣れるという意味でも学習に適した構成だと思うので、よければ参考にしてみてください。
記事中のソースコードは以下のリポジトリから参照できます。
誤りなどがありましたら、ご指摘いただけますと幸いです。
環境
- Spring Boot 3.2.2
- Java 17
- gradle-node-plugin 7.0.1
- springdoc-openapi 2.5.0
- openapi-gradle-plugin 1.8.0
- Node.js 21.6.1
- Vite 5.2.8
- openapi-generator-cli 2.13.2
- IntelliJ IDEA 2023.3.3 (Community Edition)
- Windows 11 Pro
プロジェクト構成概要
前回作成した SPA アプリに TypeScript を追加した構成のプロジェクトを構築します。
詳細は 前回の記事 を見ていただければと思いますが、簡単に説明します。
- バックエンドに
SpringBoot
(Gradle
)、フロントエンドにVue.js
(Vite
) を使う - フロントエンドプロジェクトは Gradle サブプロジェクトとして扱う
-
gradle-node-plugin
を使って、バックエンドとフロントエンドのビルドサイクルを統合する - フロントエンドのビルド成果物は
jar
ファイルに同梱し、Spring Boot
の静的リソース配信機能を利用して配信する
環境構築をスムーズに行えるように、初期状態のリポジトリを用意しました。
よろしければ、こちらを利用して環境構築を進めてください。
続いて、今回導入する具体的なライブラリやツールについて簡単に説明します。
OpenAPI
OpenAPI は、API の定義を標準化するための仕様で、API プロバイダとコンシューマ間の知識を共有する役割を担います。
この仕様は OpenAPI Specification (OAS) として知られており、API の仕様書を JSON や YAML 形式で記述します。この仕様書によって、仕様書の生成やコードの自動生成が容易になります。
詳細については、公式ドキュメントや、分かりやすく説明されている記事も多数ありますので、そちらをご参照ください。
springdoc-openapi
バックエンドでは、Spring Boot のコードから OpenAPI 仕様書を自動生成するために、springdoc-openapi を使用します。
このライブラリを使うことで、Spring の設定やクラス構造、アノテーションに基づいて、仕様書を自動生成することができます。
さらに、今回は自動生成した仕様書をサーバーに保存するために、spring-doc-openapi-gradle-plugin も併せて導入します。
OpenAPI Generator
フロントエンドでは、OpenAPI Generator を使用して API クライアントコードを生成します。
これにより、バックエンドの API を呼び出す際に、手動で HTTP リクエストを構築したり、型チェックを行う手間を省いて、よりシンプルに API を利用できるようになります。
今回の環境構築では、この OpenAPI Generator をコマンドラインから利用するために、openapi-generator-cli を導入します。このツールを使うことで、springdoc-openapi
で生成された OpenAPI 仕様書を基に、効率的に API クライアントコードを生成できます。
環境構築
前述のリポジトリをクローンして初期状態の環境が整っていることを前提に、以下のような手順で環境構築を行っていきます。
1. OpenAPI 仕様書を自動生成するための設定
2. API クライアントコードを自動生成するための設定
3. OpenAPI 仕様書と API クライアントコードを一括生成するための設定
1. OpenAPI 仕様書を自動生成するための設定
まず、Spring Boot のコードから OpenAPI 仕様書を自動生成するための設定を行います。
ルートプロジェクトの build.gradle
の plugins
と dependencies
を以下のように変更してください。
プラグイン設定
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.4'
id 'io.spring.dependency-management' version '1.1.4'
+ id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
}
依存関係設定
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.5.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
この設定により、自動生成された OpenAPI 仕様書を指定の URL (デフォルトでは /v3/api-docs
) で提供できるようになります。
確認するには、以下のコマンドで Spring Boot アプリケーションを起動してください。
$ ./gradlew bootRun
http://localhost:8080/v3/api-docs にアクセスすると、以下のような JSON 形式の仕様書が表示されることが確認できると思います。
さらに、springdoc.openapi-gradle-plugin
も併せて導入したことで、新たに generateOpenApiDocs
タスクが利用可能になっています。
このタスクを実行することで、サーバーから取得した仕様書を指定したディレクトリ (デフォルトではビルドディレクトリ) に出力できます。
実際に以下のコマンドを実行して、仕様書が出力されることを確認してみましょう。
$ ./gradlew clean generateOpenApiDocs
あらかじめ用意した初期状態のプロジェクトを利用してハンズオンを行っている方は、build/
ディレクトリに以下のような内容の JSON ファイルが出力されるはずです。
{
"openapi": "3.0.1",
"info": {
"title": "OpenAPI definition",
"version": "v0"
},
"servers": [
{
"url": "http://localhost:8080",
"description": "Generated server url"
}
],
"paths": {
"/api/hello": {
"get": {
"tags": [
"hello-controller"
],
"operationId": "getMessage",
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
},
"components": {}
}
なお、generateOpenApiDocs
タスクの実行に際して設定できるパラメータは以下の通りです。
パラメータ | 説明 | デフォルト |
---|---|---|
apiDocsUrl | 仕様書をダウンロードする URL | http://localhost:8080/v3/api-docs |
outputDir | 仕様書の出力ディレクトリ | ビルドディレクトリ |
outputFileName | 仕様書のファイル名 | openapi.json |
waitTimeInSeconds | Spring Boot アプリケーションの起動を待つ時間 (秒) | 30 秒 |
groupedApiMappings | 出力ファイル名への URL(仕様書のダウンロード元) のマップ | [] |
customBootRun | Spring Boot アプリケーションの起動に必要な bootRun プロパティ | なし |
これらの値は、openApi 拡張機能を利用することで変更できます。
OpenAPI 仕様書を自動生成するための設定はこれで終わりです。つぎは API クライアントコードを自動生成するための設定を行います。
※ workingDir にアクセスできない問題について
本来であれば、generateOpenApiDocs
タスクは追加の設定を行うことなく、デフォルトの状態で実行できるはずです。しかし、私の環境では以下のようなエラーが発生して、タスクの実行に失敗してしまいました。
* What went wrong:
Execution failed for task ':forkedSpringBootRun'.
> Cannot access input property 'workingDir' of task ':forkedSpringBootRun'. Accessing unreadable inputs or outputs is not supported. Declare the task as untracked by using Task.doNotTrackState(). For more information, please refer to https://docs.gradle.org/8.7/userguide/incremental_build.html#sec:disable-state-tracking in the Gradle documentation.
> Failed to create MD5 hash for file content.
この問題は version 1.6.0 の時点で解消されているようですが、今回使用した version 1.8.0 でも同様の問題が発生しました。
▼ Plugin fails in gradle 8.0 because of state tracking · Issue #102
以下の StackOverflow の投稿によると、Windows で Gradle を実行していることに起因する問題のようです。
▼ Gradle build error on Windows "Failed to create MD5 hash for file" - Stack Overflow
確かに、Docker コンテナ内で generateOpenApiDocs
タスクを実行すれば問題なく動作しました。
ただ、私の場合は IntelliJ IDEA を愛用しており、一度のクリックでタスクを実行できる手軽さを手放したくなかったので、 以下のように workingDir
を変更する方法で対処しました。
openApi {
customBootRun {
workingDir = File.createTempDir()
}
}
※ OpenAPI 仕様書ファイルが上書きされない問題について
OpenAPI 仕様書ファイルがすでに存在する場合、Spring Boot のコードを変更しても、その変更が仕様書に反映されません。この問題は以下の Issue に記載されていました。
▼ File is not overwritten unless explicitly removed first · Issue #25
▼ Is there any way to set it so that the output file is overwritten when you build? · Issue #131
ログを確認すると、Task :generateOpenApiDocs UP-TO-DATE
と出力され、generateOpenApiDocs
タスクがスキップされていることがわかります。
ということは、この問題の原因は Gradle のインクリメンタルビルド機能にありそうです。
インクリメンタルビルド機能は、タスクの入力や出力が前回のビルドから変更されていない場合、そのタスクを最新とみなし、実行をスキップする仕組みです。
この問題は、ビルドディレクトリにファイルを出力する場合は、./gradlew clean generateOpenApiDocs
のようにクリーンタスクを先に実行することで対処できます。しかし、ビルドディレクトリ以外の場所にファイルを出力したい場合は、この方法では解決できません。
そこで、upToDateWhen メソッドを使用して、以下のように対処しました。
tasks.named('generateOpenApiDocs') {
outputs.upToDateWhen { false }
}
この設定により、generateOpenApiDocs
タスクは常に実行されるようになるため、OpenAPI 仕様書ファイルが確実に最新の状態に更新されます。
追記
この問題は、以下の Pull Request によって修正されたようです。この変更により、クリーンタスクを実行せずに OpenAPI 仕様書ファイルを正しく上書きできるようになるはずです。
ただ、現時点で最新バージョン 1.9.0 のリリースノートにはこのプルリクエストが含まれていないため、修正が反映されていない可能性があります。
最新の修正が反映されたバージョンを確認し、必要に応じてアップデートすることをお勧めします。
追記ここまで
2. API クライアントコードを自動生成するための設定
つぎに、OpenAPI 仕様書から API クライアントコードを自動生成するための設定を行っていきます。
まずは、フロントエンドプロジェクトに openapi-generator-cli
をインストールします。
frontend/
ディレクトリに移動して、以下のコマンドを実行してください。
$ npm install @openapitools/openapi-generator-cli --save-dev
このツールを使って、バックエンドで提供された OpenAPI 仕様書から TypeScript で記述された Axios ベースの API クライアントコードを生成するには、以下のコマンドを実行します。
openapi-generator-cli generate -g typescript-axios -i ../build/openapi.json -o ./src/api
オプション
-
-g
: テンプレートエンジンを指定 -
-i
: OpenAPI 仕様書ファイルのパスを指定 -
-o
: 生成コードの出力先ディレクトリを指定
コマンドの実行に成功すると、frontend/src/
ディレクトリに以下のようなファイル群が生成されることが確認できると思います。
src/
├── api/
│ ├── .openapi-generator
│ │ ├── FILES
│ │ └── VERSION
│ ├── .gitignore
│ ├── .npmignore
│ ├── .openapi-generator-ignore
│ ├── api.ts
│ ├── base.ts
│ ├── common.ts
│ ├── configuration.ts
│ ├── git_push.sh
│ └── index.ts
└── openapitools.json
フロントエンドからバックエンドの API を呼び出す際は、この自動生成されたクライアントコードを活用することになります。
このコマンドは、npm
スクリプトとして追加しておきましょう。
frontend/package.json
の scripts
プロパティに、以下のように追加してください。
"scripts": {
...
+ "generate-api-client": "openapi-generator-cli generate -g typescript-axios -i ../build/openapi.json -o ./src/api"
}
これにより、以下ようにスクリプト名だけを使って簡単に実行できるようになります。
$ npm run generate-api-client
さて、これで当初の目的は達成しましたが、せっかくなのでひとつのコマンドで OpenAPI 仕様書と API クライアントコードの両方が生成・更新できるようにしちゃいましょう。
すでに準備は整っているので、あとはビルドファイルを少し修正するだけです。
3. OpenAPI 仕様書と API クライアントコードを一括生成するための設定
それでは、OpenAPI 仕様書の生成と同時に API クライアントコードも自動生成されるように設定を行っていきます。
3.1. タスク定義
まず、OpenAPI 仕様書と API クライアントコードの生成をまとめて行うタスクを定義します。
ルートプロジェクトの build.gradle
に、以下を追加してください。
tasks.register("generateOpenApiDocsAndApiClient") {
dependsOn generateOpenApiDocs, ":frontend:npm_run_generate-api-client"
}
新しく定義した generateOpenApiDocsAndApiClient
タスクには、generateOpenApiDocs
タスクと npm_run_generate-api-client
タスクの両方を依存関係として追加しています。
npm_run_generate-api-client
タスクは、npm スクリプト generate-api-client
を実行するための Gradle タスクです。
frontend
サブプロジェクトでは、gradle-node-plugin を利用しているため、このように特別な設定を追加することなく、npm コマンドを Gradle タスクとして実行できます。gradle-node-plugin
の詳細については、こちらの記事をご参照ください。
3.2. タスク実行順序の設定
つぎに、OpenAPI 仕様書の生成が完了した後に API クライアントコードが生成されるように、タスクの実行順序を設定しましょう。
frontend
サブプロジェクトの build.gradle
に、以下を追加してください。
tasks.named("npm_run_generate-api-client") {
mustRunAfter(rootProject.tasks.named("generateOpenApiDocs"))
}
これにより、npm_run_generate-api-client
タスクは必ず generateOpenApiDocs
タスクの後に実行されるようになります。
この 2 つの設定を行うことで、個別にタスクを実行する手間を省きつつ、実行順序を確実に保証することができます。
なお、:frontend:npm_run-generate-api-client
タスクに直接 generateOpenApiDocs
への依存関係を追加する方法も考えられますが、その場合、各タスクを個別に実行することができなくなってしまいます。
そのため、新しいタスクを定義し、mustRunAfter
を用いて実行順序を制御する方法を取ることで、タスクを柔軟に実行できるようにしています。タスク実行順序に関する詳細な情報については、Gradle 公式ドキュメントの Controlling Task Execution ページをご参照ください。
設定が完了したら、以下のコマンドを実行して、generateOpenApiDocsAndApiClient
タスクが正しく動作し、仕様書と API クライアントコードの両方が生成できるかを確認しましょう。
$ ./gradlew generateOpenApiDocsAndApiClient
前の手順で既にファイルが生成されている場合は、一度それらを削除してから再実行すると、タスクが正常に動作しているかを確実に確認できます。
この設定により、例えば以下のような作業フローが構築できるようになります。
1. Spring Boot のコードを追加・修正する
2. generateOpenApiDocsAndApiClient
タスクを実行し、OpenAPI 仕様書と API クライアントコードを最新の状態に更新する
3. 新しい API クライアントコードを使ってフロントエンドの実装を進める
このフローを繰り返すことで、バックエンドとフロントエンドの変更を追従しながらスムーズに開発を進められます。互いの変更を手作業で同期する必要がなくなり、手戻りや無駄なコーディングも最小限に抑えられるでしょう。
環境構築の説明は以上で終わりです。
おまけ. 動作確認
さいごに、既存のサンプルコードを API クライアントコードを使った実装に変更する方法を簡単に紹介します。
frontend/src/main/components/HelloWorld.vue
を以下のように変更してください。
<script setup lang="ts">
import { ref, onMounted } from 'vue';
- import axios from 'axios';
+ import { HelloControllerApi } from '@/api';
const message = ref('');
onMounted(async () => {
- try {
- const response = await axios.get('/api/hello')
- message.value = response.data
- } catch (error) {
- console.error('Error:', error)
- }
+ const api = new HelloControllerApi();
+ const response = await api.getMessage();
+ message.value = response.data;
});
</script>
<template>
...
</template>
<style scoped>
...
</style>
axios
を使用して直接 HTTP リクエストを送信していた部分を、OpenAPI Generator によって自動生成された HelloControllerApi
クラスを使用するように変更しました。
OpenAPI Generator によって生成された API クライアントコードは、サーバーとの通信に関する型情報を含んでいます。そのため、IDE の補完機能が活用できるようになり、コンパイル時にはエラーを検出しやすくなります。
さらに、生成されたクライアントコードを使用することで、エンドポイントやパラメータの変更に対する保守性が向上し、一貫したエラーハンドリングもできるようになります。
さいごに
いかがでしたでしょうか。
私としては想像している以上に簡単にできたので、びっくりしました。
何事もやってみないとわかりませんね。
それでは、最後までお読みいただき、ありがとうございました!