複数ファイルに対応したSwagger UIを作る
この記事は?
swagger-uiは2022年現在おそらく一番流行っているAPIドキュメント作成ライブラリ。
遅ればせながら導入してみたものの、標準搭載の機能では下記の実現がうまくいかなかった。。。
- 特定のディレクトリに格納されているドキュメントファイルをすべてUIに表示する
- ドキュメントファイルをUI上でリスト表示し、検索性を高める
- ドキュメントファイルに変更が加わったら自動的にUIを更新する
エンジニアなら、ないものは作るか、ということで独自にカスタマイズした方法を書き留める。
ステップ1:ViteでReactアプリケーションを構築する
Swagger UIはReactで作成されているため、Reactを使ってカスタマイズするためのパッケージswagger-ui-reactが公開されている。
これを拡張していくにあたってシンプルにReactアプリケーションだけを組んでもよいけど、下記の理由からビルドツール(Vite)を利用した方がよさそう。
- プロジェクト内に格納されているドキュメントファイルのパスをシステム内で取得したい(サーバーサイド処理が必要)
- ドキュメントファイルへの変更をリアルタイムに検知して更新したい(HMRが必要)
Vite + Reactのアプリケーション作成はnpm create vite@latest
を使えば誰でも簡単にできるので詳細は割愛。(Viteの公式ドキュメントを見てね)
ステップ2:全ドキュメントファイルのパスをReactから参照できるようにする
下記のようなディレクトリ構成を想定して話を進めます。
root/
┣ src/
┃ ┣ components/
┃ ┃ ┗ App.jsx
┃ ┗ main.jsx
┣ doc/
┃ ┣ group-a/
┃ ┃ ┗ swagger-config-1.yml
┃ ┗ group-b/
┃ ┗ swagger-config-2.yml
┗ vite.config.js
パスの取得
まずすべてのAPIドキュメントファイルのパスはglobを使って配列として取得。
globなどのファイル検索はサーバーサイドでしか実行できないため、vite.config.js
上に記述してNode.jsから実行させる。
const swaggerFilePaths = glob
.sync(resolve(__dirname, 'doc/**/*.yml'))
.map((url) => `/${relative(__dirname, url)}`)
後ほどSwagger UIからドキュメントファイルのパスを参照させる際にvite.config.js
の属するディレクトリからの相対パスを指定しておく。
パス配列をアプリケーションへ渡す
上記で取得したswaggerFilePaths
をViteからReactへ渡すよう設定。
export default defineConfig({
...
define: {
'import.meta.env.SWAGGER_FILE_PATHS': JSON.stringify(swaggerFilePaths)
},
...
})
これで、Reactアプリケーション内からimport.meta.env.SWAGGER_FILE_PATHS
という名前でswaggerFilePaths
を参照できるようになった。
define
で設定した値はアプリケーションへ渡される際にJSONとして解析された上でglobalThis
に定義されることに注意。
キー名は何でも良いのだけど、Viteで定義したことを明示するためimport.meta.env
のプロパティとして定義しておくとわかりやすいと思う。
ステップ3:ドキュメントファイルのパスをSwaggerUIに設定する
ステップ2でアプリケーション内に定義したimport.meta.env.SWAGGER_FILE_PATHS
からドキュメントファイルのパスを取り出してswagger-ui-reactで定義されているコンポーネントに設定。
下記は初期表示時にSWAGGER_FILE_PATHS
の第1要素を設定する場合の例。
import { useEffect, useState } from 'react'
import SwaggerUI from 'swagger-ui-react`
export default function App() {
const [url, setUrl] = useState('')
const swaggerFilePaths = import.meta.env.SWAGGER_FILE_PATHS
useEffect(() => {
setUrl(swaggerFilePaths[0] ?? '')
}, [])
return <SwaggerUI url={url} />
}
ここまででとりあえずSwagger UIを画面上で確認できる状態になった。
ステップ4:ドキュメントファイルの変更を検知できるようにする
OpenAPI用ファイルの更新を検知してくれるプラグイン(rollup-plugin-openapi)があるのでこれを適用しておく。
ViteをHMRで起動すれば、<SwaggerUI>
のurl
パラメータに設定したYAMLが更新されると勝手に画面を更新してくれるので入れておくと便利。
import openapi from 'rollup-plugin-openapi'
...
export default defineConfig({
...
plugins: [react(), openapi()],
...
})
ステップ5:配列で受け取ったドキュメントファイルパスをUI上でリスト化する
ルーティング設定
ドキュメントごとに画面のURLを切り替えられるようルーティング設定。
Viteで定義したimport.meta.env.SWAGGER_FILE_PATHS
の各要素は/doc/**/*.yml
フォーマットの文字列になっている。
ファイルごとに一意なパスを用意したいので、/doc
と拡張子を除いた/**/*
部分をパスパラメータとして設定おくと扱いやすそう。
import {
createBrowserRouter,
redirect,
Outlet,
RouterProvider,
LoaderFunction,
} from 'react-router-dom'
const routePaths = import.meta.env.SWAGGER_FILE_PATHS.map((filePath) =>
filePath.replace(/^\/doc(\/[^\.]+)\.yml$/, (_, routePath) => routePath)
)
const loader = ({ params }) => {
const param = params['*'] ?? ''
if (param && routePaths.includes(param)) return null
// 存在しないパスにアクセスしようとした場合は、1つ目のドキュメントファイルにリダイレクトさせる
return redirect(routePaths[0])
}
const router = createBrowserRouter([
{
path: '/',
loader,
element: <Outlet />,
children: [
{
path: '*',
loader,
element <App />
}
]
}
])
ReactDOM.createRoot(document.getElementById('app')).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
)
ルーティング情報を用いて表示するドキュメントファイルを動的に切り替える
上記によってパスパラメータからドキュメントファイルのパスを取得できるようになった。
これを<SwaggerUI>
コンポーネントに動的に設定。
import { useParams } from 'react-router-dom'
...
export default function App() {
...
const params = useParams()
useEffect(() => {
const currentRoute = params['*']
setUrl(`/doc/${currentRoute}.yml`)
}, [params])
return <SwaggerUI url={url} />
}
後はimport.meta.env.SWAGGER_FILE_PATHS
から取得した全ファイルをReactでリスト化すれば、UI上でドキュメントを指定して表示することができる。
まとめ
以上、Vite + React + Swagger UIでドキュメントを動的に選択・表示できるUIを作る方法を簡単に記載してみた。
実際にはTypeScriptで型定義をしたり、MUIなどを用いてUIをカスタマイズしたりするともっと便利になるけどその辺は省略。
個人的な感想として、普段はフレームワークとしてVueを使用することが多く、当初はVueでの構築も検討したものの、Swagger UIがReactベースである以上、型定義や挙動の競合が発生しそうだったので断念。
久しぶりにReactを触ったけど、これくらいの規模ならReactの方が書きやすかった。
Discussion