LoginSignup
1
2

SpringBootで包み込むVue.js開発環境 カンタン構築 OpenAPI 編

Posted at

はじめに

前回の記事で、SpringBoot (Gradle) を使ったバックエンドと、Vue.js (Node.js) を使ったフロントエンドで構成される SPA の環境構築手順を紹介しました。

この構成で、さらにフロントエンドに TypeScript を導入しようと考えたのですが、SPA や TypeScript が初めての私は、真っ先に「バックエンドとフロントエンドで別々に型定義するってことは手間が増えるなぁ」「いかにも修正漏れしそうだなぁ」と思ってしまいました。

そもそも、小規模なら手動で型定義を同期するコストは高くないですし、フロントエンドとバックエンドが密に連携できれば API の仕様変更に追従しやすいので、面倒がるほどでもありません。
それでもいい解決策があればと思ってあれこれ調べるうちに、OpenAPI にたどり着きました。

私の要件は、つまるところ以下の 2 点です。

  1. フロントエンドからバックエンドの API に型安全に接続したい
  2. 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 の仕様書を JSONYAML 形式で記述し、それを基にドキュメントやコードの自動生成などを行うことができます。

詳細については、公式ドキュメントや、分かりやすく説明されている記事も多数ありますので、そちらをご参照いただければと思います。

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 に以下を追加してください。

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.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": {}
}

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 を変更する方法で対処しました。

build.gradle (ルートプロジェクト)
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 を使用して、以下のように対処しました。

build.gradle (ルートプロジェクト)
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.jsonscripts プロパティに以下を追加してください。

package.json
"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 に、以下を追加してください。

build.gradle (frontend サブプロジェクト)
tasks.named("npm_run_generate-api-client") {
    mustRunAfter(rootProject.tasks.named("generateOpenApiDocs"))
}
build.gradle (ルートプロジェクト)
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 を以下のように変更してください。

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 の補完機能を活用することができるため、開発効率も向上します。

さいごに

いかがでしたでしょうか。

私としては想像している以上に簡単にできたので、びっくりしました。
何事もやってみないとわかりませんね。

それでは、最後までお読みいただき、ありがとうございました!

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2