はじめに
前回の記事で、SpringBoot (Gradle) を使ったバックエンドと、Vue.js (Node.js) を使ったフロントエンドで構成される SPA の環境構築手順を紹介しました。
この構成で、さらにフロントエンドに TypeScript を導入しようと考えたのですが、SPA や TypeScript が初めての私は、真っ先に「バックエンドとフロントエンドで別々に型定義するってことは手間が増えるなぁ」「いかにも修正漏れしそうだなぁ」と思ってしまいました。
そもそも、小規模なら手動で型定義を同期するコストは高くないですし、フロントエンドとバックエンドが密に連携できれば API の仕様変更に追従しやすいので、面倒がるほどでもありません。
それでもいい解決策があればと思ってあれこれ調べるうちに、OpenAPI にたどり着きました。
私の要件は、つまるところ以下の 2 点です。
- フロントエンドからバックエンドの API に型安全に接続したい
- API クライアントコードを自動生成したい
OpenAPI を導入すれば、バックエンドのコードから OpenAPI ドキュメントを自動生成でき、さらにそのドキュメントからフロントエンドで利用する API クライアントコードを自動生成できるので、この要件が満たせそうです。
実際に試してみると、悩みは無事解決し、OpenAPI の知識がゼロの状態からでもスムーズに導入できたので、今回はこの構成で OpenAPI を導入する手順を紹介しようと思います。
実用的ではないかもしれませんが、OpenAPI の仕組みを理解し、ツールに慣れるという意味では、学習に最適の構成だと思うので、よろしければ参考にしてみてください。
記事中のソースコードは以下のリポジトリから参照できます。
誤りなどがありましたら、ご指摘いただけますと幸いです。
環境
- Java 17
- Spring Boot 3.2.2
- gradle-node-plugin 7.0.1
- springdoc-openapi 2.5.0
- openapi-gradle-plugin 1.8.0
- Node.js 21.6.1
- 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 の設定、クラス構造、アノテーションに基づいた OpenAPI ドキュメントを自動生成することができます。
今回は OpenAPI ドキュメントをサーバに保存するために spring-doc-openapi-gradle-plugin も併せて導入します。
OpenAPI Generator
フロントエンドでは、openapi-generator-cli を利用します。
OpenAPI Generator は、OpenAPI ドキュメントから様々な言語のサーバおよび API クライアントコードを生成するためのツールです。
OpenAPI Generator によって生成された API クライアントコードを使えば、フロントエンドからバックエンド API を呼び出す際に手動で HTTP リクエストを構築したり、型チェックをしたりする必要がなくなるため、シンプルな呼び出しが可能になります。
なお、OpenAPI Generator には複数のテンプレートが用意されていますが、今回は TypeScript
で記述された Axios
ベースの API クライアントコードを生成したいため、typescript-axios
を使用します。
環境構築
以下のような手順で環境を構築していきます。
1. OpenAPI ドキュメントを自動生成するための設定
2. API クライアントコードを自動生成するための設定
3. OpenAPI ドキュメントと API クライアントコードを一括生成するための設定
1. OpenAPI ドキュメントを自動生成するための設定
まず、Spring Boot のコードから OpenAPI ドキュメントを自動生成するための設定を行います。
ルートプロジェクトの build.gradle
に以下を追加してください。
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"
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
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'
}
tasks.named('test') {
useJUnitPlatform()
}
tasks.named('processResources') {
dependsOn(":frontend:npm_run_build")
}
※ springdoc-openapi
には、Swagger UI
を提供するライブラリ springdoc-openapi-starter-webmvc-ui
と、提供しないライブラリ springdoc-openapi-starter-webmvc-api
の 2 種類あります。このハンズオンはどちらでも適用可能なので、お好みの方を選択してください。
springdoc-openapi
を依存関係に追加することで、Spring Boot アプリケーションから自動生成された OpenAPI ドキュメントを指定の URL (デフォルトでは /v3/api-docs
) から提供できるようになります。
実際に以下のコマンドで Spring Boot アプリケーションを起動して、http://localhost:8080/v3/api-docs にアクセスすると、JSON
形式のドキュメントが確認できるかと思います。
$ ./gradlew bootRun
なお、今回は OpenAPI ドキュメントをサーバ上で扱うため、springdoc.openapi-gradle-plugin
も併せて導入しています。
このプラグインを使えば、generateOpenApiDocs
タスクを実行するだけで、OpenAPI ドキュメントファイルをサーバから取得し、指定したディレクトリに出力することができます。
generateOpenApiDocs
タスクの実行に際して設定できるパラメータは以下の通りです。
パラメータ | 説明 | 必須 | デフォルト |
---|---|---|---|
apiDocsUrl | OpenAPI ドキュメントをダウンロードする URL | No | http://localhost:8080/v3/api-docs |
outputDir | OpenAPI ドキュメントの出力ディレクトリ | No | ビルドディレクトリ |
outputFileName | OpenAPI ドキュメントのファイル名 | No | openapi.json |
waitTimeInSeconds | Spring Boot アプリケーションの起動を待つ時間 (秒) | No | 30 秒 |
groupedApiMappings | 出力ファイル名への URL(OpenAPI ドキュメントのダウンロード元) のマップ | No | [] |
customBootRun | Spring Boot アプリケーションの起動に必要な bootRun プロパティ | No | なし |
これらの値は、openApi 拡張機能を利用することで変更できます。
試しに以下のコマンドを実行して、OpenAPI ドキュメントが出力されることを確認してみましょう。
$ ./gradlew clean generateOpenApiDocs
あらかじめ用意した初期状態のプロジェクトを利用してハンズオンを行っている場合、build/
ディレクトリに以下のような内容の OpenAPI ドキュメントファイルが出力されていれば成功です。
{
"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": {}
}
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
おそらく、この問題の原因は Gradle がビルド効率化のために、依存するファイルやタスクの出力に変更がない場合にタスクを再実行しないことにあります。
実際にログを確認すると、> Task :generateOpenApiDocs UP-TO-DATE
と出力され、generateOpenApiDocs
タスクがスキップされていることがわかります。
この問題は、ビルドディレクトリにファイルを出力する場合は、./gradlew clean generateOpenApiDocs
のようにクリーンタスクを先に実行することで対処できます。
しかし、ビルドディレクトリ以外の場所にファイルを出力したい場合は、この方法では解決できません。
そこで、upToDateWhen を使用して、以下のように対処しました。
tasks.named('generateOpenApiDocs') {
outputs.upToDateWhen { false }
}
この設定により、generateOpenApiDocs
タスクは常に実行されるようになるため、OpenAPI ドキュメントファイルが確実に最新の状態に更新されます。
2. API クライアントコードを自動生成するための設定
つぎに、OpenAPI ドキュメントから API クライアントコードを自動生成するための設定を行っていきます。
まずは、フロントエンドプロジェクトに openapi-generator
をインストールします。
frontend/
ディレクトリに移動して、以下のコマンドを実行してください。
$ npm install @openapitools/openapi-generator-cli --save-dev
OpenAPI Generator CLI
を使って、バックエンド側で提供された OpenAPI ドキュメントから TypeScript で記述された Axios ベースの API クライアントコードを生成するには、以下のコマンドを実行します。
openapi-generator-cli generate -g typescript-axios -i ../build/openapi.json -o ./src/api
オプション
-
-g
: テンプレートエンジンを指定 -
-i
: OpenAPI ドキュメントファイルのパスを指定 -
-o
: 生成コードの出力先ディレクトリを指定
このコマンドは、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
コマンドの実行に成功すると、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 を呼び出す際は、この自動生成されたクライアントコードを活用することになります。
これで当初の目的は達成しましたが、せっかくなのでひとつのコマンドで OpenAPI ドキュメントと API クライアントコードの両方が生成・更新できるようにしちゃいましょう。
手間が省けて、ミスも防げますからね!すでに準備は整っているので、あとはビルドファイルを少し修正するだけです。
3. OpenAPI ドキュメントと API クライアントコードを一括生成するための設定
最後に、OpenAPI ドキュメントの生成と同時に API クライアントコードも自動生成されるよう設定を行っていきます。
各プロジェクトの build.gradle
に、以下を追加してください。
tasks.named("npm_run_generate-api-client") {
mustRunAfter(rootProject.tasks.named("generateOpenApiDocs"))
}
tasks.register("generateOpenApiDocsAndApiClient") {
dependsOn generateOpenApiDocs, ":frontend:npm_run_generate-api-client"
}
ここでは、以下の 2 つの設定を行っています。
1. OpenAPI ドキュメントを生成するタスクと API クライアントコードを生成するタスクの実行順序を設定する
2. これら 2 つのタスクを含む新しいタスクを定義する
それでは、実際に generateOpenApiDocsAndApiClient
タスクを実行して、OpenAPI ドキュメントと API クライアントコードの両方が生成されることを確認してみましょう。
$ ./gradlew generateOpenApiDocsAndApiClient
これにより、たとえば以下のようなバックエンドの API の変更をフロントエンドに反映させる作業フローが構築できます。
1. Spring Boot のコードを追加・修正する
2. generateOpenApiDocs タスクを実行し、OpenAPI ドキュメントと API クライアントコードを最新の状態に更新する
3. 新しい API クライアントコードを使ってフロントエンドの実装を進める
このフローを繰り返すことで、バックエンドとフロントエンドの変更を追従しながらスムーズに開発を進められます。お互いの変更を手作業で同期する必要がなくなり、手戻りや無駄なコーディングを最小限に抑えられます。
ちなみに、これだけの設定で済むのは、フロントエンド用サブプロジェクトに gradle-node-plugin が設定されているためです (詳細は 前回の記事 を参照)。
このプラグインを使用すると、npm コマンドをそのまま Gradle タスクとして実行できます。npm コマンドの単語の区切りをアンダースコアに変換するだけの単純なルールで Gradle タスクに自動変換されるため、追加の設定は必要ありません。
そのため、手順2. で追加した generate-api-client
スクリプトは npm_run_generate-api-client
タスクとして実行可能になります。
環境構築の説明は以上です。
おまけ. 動作確認
せっかくなので、既存のサンプルコードを 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)
- }
- })
+ onMounted(async () => {
+ 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 の補完機能を活用することができるため、開発効率も向上します。
さいごに
いかがでしたでしょうか。
私としては想像している以上に簡単にできたので、びっくりしました。
何事もやってみないとわかりませんね。
それでは、最後までお読みいただき、ありがとうございました!