NestJSのコード変更を検知して、フロントエンドで使う型定義、SWR、MSWのコードを自動生成する(Orval)
紹介すること
NestJSで書かれたREST APIのコードに変更があった際、フロントエンドの関連するコードも自動で更新する
バックエンドのコードからOpenAPIを生成し、OpenAPIからフロントエンドで使用する型定義、Axios, SWR、MSWのコードを自動生成する
OpenAPIを経由して各種自動生成を行うので、使用技術に変更があっても対応しやすい
具体的な方法は下記の順番で紹介する
- NestJSのコードからOpenAPIを生成する
NestJS SwaggerModuleを使用 - OpenAPIからフロントエンドのコードを生成する
Orvalを使用 - OpenAPI変更の差分検知とコードの自動生成
GitHub Actionsを使用
プロジェクト環境
- フロントエンドはReact、バックエンドはNestJSで実装しているプロジェクト
- 同一のGitHubリポジトリでフロントエンドとバックエンドのコードを管理
- REST API
- API通信にSWRを使用
- モックサーバーにMSWを使用
1. NestJSのコードからOpenAPIを生成する
NestJSのコードからOpenAPI(Swagger)を生成する
NestJS SwaggerModuleで出来ること
@nestjs/swaggerを使用することで、ソースコードからOpenAPIを生成する
NestJSのエントリーポイントでSwaggerModuleのsetup関数を呼ぶことで、Swagger UI、OpenAPIのJSON、YAMLファイルにアクセスできるようになる
NestJSのコードからOpenAPIが生成できるようにする
必要なデコレータを各所(主にcontrollerやdto)に付与していくと、ある程度自動でOpenAPIが生成され、自動では上手く行かない箇所も手動でデコレータを付与することで、NestJSのソースコードからOpenAPIが生成出来る
NestJSで使用しているライブラリやコードの書き方によってうまくOpenAPIのリクエスト、レスポンスパラメータが記述出来ないことがあり、苦戦することが多かったが、詳細を紹介すると長くなってしまうので今回は割愛
最終的には全て乗り越えて意図した通りOpenAPIが生成出来た
OpenAPIのJSONファイルを出力するコードとコマンドを作成する
サーバーを立ち上げていれば、特定のURLからJSONファイルをダウンロード出来るが、GitHub Actions上でサーバーを立ち上げるのは面倒で、出来たとしてもCIの実行時間がかかってしまうので、サーバーを立ち上げずにJSONを生成できるようにする
OrvalはYAMLからも生成出来るので、YAMLを生成しても良い
2024年9月時点ではNestJSのSwaggerModuleでは、サーバーを立ち上げないとOpenAPIのJSONファイルは生成出来ないので、サーバーを立ち上げてJSONファイルを吐き出し、サーバーを終了するコードとコマンドを用意する
backend/apps/api-server/src/open-api/setupOpenApi.ts
// backend/apps/api-server/src/open-api/setupOpenApi.ts
import { INestApplication } from '@nestjs/common'
import { SwaggerModule, DocumentBuilder, OpenAPIObject } from '@nestjs/swagger'
export const setupOpenApi = async (
app: INestApplication
): Promise<OpenAPIObject> => {
const config = new DocumentBuilder()
.setTitle('API Server')
.setDescription('The API description')
.setVersion('1.0')
.addBearerAuth()
.build()
const document = SwaggerModule.createDocument(app, config)
SwaggerModule.setup('api', app, document)
return document
}
backend/apps/api-server/src/open-api/generate-open-api-json
// backend/apps/api-server/src/open-api/setupOpenApi.ts
import { exec } from 'child_process'
import { writeFileSync } from 'fs'
import { resolve } from 'path'
import { NestFactory } from '@nestjs/core'
import { setupOpenApi } from '@root/apps/api-server/src/open-api/open-api-setup'
import { registerGlobals } from '@root/apps/closed-api-server/src/registerGlobals'
import { ClosedApiServerModule } from 'apps/api-server/src/api-server.module'
async function bootstrap() {
const app = await NestFactory.create(ClosedApiServerModule)
registerGlobals(app)
const document = await setupOpenApi(app)
const outputPath = resolve(
process.cwd(),
'./apps/api-server/src/open-api/open-api.json'
)
writeFileSync(outputPath, JSON.stringify(document), { encoding: 'utf8' })
exec(`prettier --write ${outputPath}`) // 差分が見やすいようにフォーマットしている
await app.close()
}
bootstrap()
backend/package.json
// backend/package.json
{
...
"scripts": {
...
"gen-open-api": "nest start --entryFile /open-api/generate-open-api-json"
}
...
}
npm run gen-open-api
を実行すると、NestJSのコードを元に作成されたOpenAPIのJSONがbackend/apps/api-server/src/open-api/open-api.json
が出力される
2. OpenAPIからフロントエンドのコードを生成する
Orvalできること
OrvalはOpenAPI v3やSwagger v2のJSONやYAMLを元にコードを自動生成してくれるツール。現在も活発にアップデートが進んでいる
今回使用するSWR, MSW, 型定義だけでなく、React query, Zod, Angular, Hono等サポートしている範囲は広い
特に凝ったことをやらなけば、OpenAPIのパス指定、出力先、その他簡単な設定をすればコードが自動生成できる
今回はOrvalが標準で出来ることに加えて、少しだけコードを好きな形に加工する
Orvalの設定ファイルを作成する
実行前にOpenAPIを加工する
OpenAPIを作る時に工夫すれば解決することもあるが、フロントエンドの都合でOrvalが出力するコードをコントロールしたい箇所もあるので、OrvalのInput.transformerで加工するコードを作成する
- 対象とするエンドポイントを/api/v2のみに絞る
- フロントエンドから呼ばないエンドポイントは除外したいため
- フロントエンドに出力するディレクトリ名からは/v2を除く
- 全てのリクエストのクエリパラメーターに付与する
tenantId
を取り除く-
tenantId
を都度付与するのが面倒なので、APIリクエストをする共通処理で付与したいが、ここで取り除かないと型定義に含まれてしまうため
-
web/settings/orval/input-transformer.js
// web/settings/orval/input-transformer.js
module.exports = inputSchema => {
return {
...inputSchema,
paths: getPaths(inputSchema.paths)
}
}
const API_V2_PATH = '/api/v2'
const selectV2Endpoints = paths => {
const filteredPaths = Object.fromEntries(
Object.entries(paths).filter(([path]) => path.startsWith(API_V2_PATH))
)
return filteredPaths
}
const removeTenantIdParameter = paths => {
return Object.fromEntries(
Object.entries(paths).map(([path, methods]) => [
path,
Object.fromEntries(
Object.entries(methods).map(([method, details]) => [
method,
{
...details,
parameters: details.parameters
? details.parameters.filter(param => param.name !== 'tenantId')
: details.parameters
}
])
)
])
)
}
const removeV2PrefixFromTags = paths => {
return Object.fromEntries(
Object.entries(paths).map(([path, methods]) => [
path,
Object.fromEntries(
Object.entries(methods).map(([method, details]) => [
method,
{
...details,
tags: details.tags
? details.tags.map(tag => tag.replace('v2/', '')) // 出力されるファイルやディレクトリにv2がつかないようにする
: details.tags
}
])
)
])
)
}
const getPaths = paths => {
const v2Paths = selectV2Endpoints(paths)
const pathsWithoutTenantId = removeTenantIdParameter(v2Paths)
return removeV2PrefixFromTags(pathsWithoutTenantId)
}
customInstanceを作成する
OrvalはAPIリクエストで使うfetchやAxiosのインスタンスを指定することが出来る
今回はSWRを使用するが、SWRのfetcherのコードで使用するAxiosのインスタンスで下記の処理を行う
- クエリパラメータの加工
- クエリパラメータに
tenantId
を付与 - Object型や配列型を加工する
- 利用側でコンバートすると型定義と合わなくなるので、ここで行う
- クエリパラメータに
- axiosのリクエスト、レスポンスインターセプタの差し込み
web/src/lib/axios/customInstance.ts
// web/src/lib/axios/customInstance.ts
import Axios, { type AxiosError, type AxiosRequestConfig } from 'axios'
import { addRequestInterceptor } from 'src/lib/axios/addRequestInterceptor'
import { addResponseInterceptor } from 'src/lib/axios/addResponseInterceptor'
import { getFirebaseService } from 'src/lib/firebase'
import { isObject } from 'src/lib/validator'
import { store } from 'src/stores/store'
export const AXIOS_INSTANCE = Axios.create({
baseURL: '/', // baseURLを指定してしまうとMSWのパスとマッチしなくなるので指定しない。自動生成なのでbaseURLが変わっても手間なく変更できる
paramsSerializer: params => {
/**
* sample=['1','2','3']をsample=1%2C2%2C3
* 型定義的にオブジェクト型の場合
**/
const searchParams = new URLSearchParams(
Object.entries(params)
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => {
if (isObject(value)) {
return [key, JSON.stringify(value)]
}
return [key, value.toString()]
})
)
return searchParams.toString()
}
})
/** @public */
export const customInstance = <T>(
config: AxiosRequestConfig,
options?: AxiosRequestConfig
): Promise<T> => {
const { auth } = getFirebaseService()
addRequestInterceptor(AXIOS_INSTANCE, auth)
addResponseInterceptor(AXIOS_INSTANCE)
const promise = AXIOS_INSTANCE({
...config,
params: {
clientId: store.getState().client.selectedClientId,
...config.params
},
...options
}).then(({ data }) => data)
return promise
}
// In some case with react-query and swr you want to be able to override the return error type so you can also do it here like this
/** @public */
export type ErrorType<Error> = AxiosError<Error>
/** @public */
export type BodyType<BodyData> = BodyData
生成されたコードを加工する
OrvalのHooks.afterAllFiledを使えば生成されたコードを加工することが出来る
自動生成するコードは手動で書き換えても次回の実行時に上書きされてしまうので、Orvalで生成する一連の処理に組み込んで加工する
今回は下記3点を行う
- SWRのカスタムインスタンスを利用する
- 通常SWRConfigでSWRのグローバル設定は行うが、SWRConfigがネスト出来ないようにしておきたいので、カスタムインスタンスを使うようにする
-
@public
アノテーションを付与する- eslint-plugin-import-accessを使用して、インポートを制御しているため
- リントとフォーマットの実行
- 頻繁に読むコードなので、フォーマットしておきたいため
- 自動生成されるコードなのでリントは不要だがimportの順番制御をリンターで行っているのと、フォーマットを行うならついでに実行する
ts-morphを使ってコードを書き換える
あまり凝ったことをやるとOrvalのバージョンアップなどで対応出来なくなる可能性がでてきたり、何をやっているのかわからなくなってしまうが、今回行いたいことはシンプルなのでやってしまう
web/settings/orval/hooks/index.ts
// web/settings/orval/hooks/index.ts
import { Project } from 'ts-morph'
import { exec } from 'child_process'
import { promisify } from 'util'
import { imporSwrCustomInstance } from './importSwrCustomInstance'
import { addPublicJsDocs } from './add-public-annotation'
const execAsync = promisify(exec)
const project = new Project()
const modifyFiles = async () => {
const sourceFiles = project.addSourceFilesAtPaths([
'./src/api/clients/**/*.ts',
'./src/api/schemas/**/*.ts'
])
sourceFiles.forEach(sourceFile => {
imporSwrCustomInstance(sourceFile) // SWRのカスタムインスタンスを使用する
addPublicJsDocs(sourceFile) // publicアノテーションを付与する
sourceFile.saveSync()
})
await project.save()
}
;(async () => {
await modifyFiles()
await execAsync(
"npx eslint --cache --fix 'src/api/{clients,schemas}/**/*.ts'"
)
await execAsync("npx prettier --write 'src/api/{clients,schemas}/**/*.ts'")
})()
web/settings/orval/hooks/importSwrCustomInstance.ts
import { SourceFile } from 'ts-morph'
export const imporSwrCustomInstance = (sourceFile: SourceFile) => {
sourceFile.getImportDeclarations().forEach(importDeclaration => {
if (importDeclaration.getModuleSpecifierValue() === 'swr') {
importDeclaration.setModuleSpecifier('src/lib/swr/instance/swr')
}
if (importDeclaration.getModuleSpecifierValue() === 'swr/mutation') {
importDeclaration.setModuleSpecifier('src/lib/swr/instance/swrMutation')
}
})
}
web/settings/orval/hooks/add-public-annotation.ts
import { SourceFile, TypeAliasDeclaration, VariableStatement } from 'ts-morph'
export const addPublicJsDocs = (sourceFile: SourceFile) => {
addPublicJsDocToExportedVariables(sourceFile)
addPublicJsDocToExportedTypes(sourceFile)
}
const addPublicJsDocToExportedVariables = (sourceFile: SourceFile) => {
sourceFile.getVariableStatements().forEach(addPublicJsDocToExportKeyword)
}
const addPublicJsDocToExportedTypes = (sourceFile: SourceFile) => {
sourceFile.getTypeAliases().forEach(addPublicJsDocToExportKeyword)
}
const addPublicJsDocToExportKeyword = (
variable: VariableStatement | TypeAliasDeclaration
) => {
if (!variable.hasExportKeyword()) {
return
}
const jsDocs = variable.getJsDocs()
if (jsDocs.length === 0) {
variable.addJsDoc('@public')
} else {
jsDocs.forEach(jsDoc => {
const text = jsDoc.getText()
if (!text.includes('@public')) {
jsDoc.insertTag(0, { tagName: 'public' })
}
})
}
}
Orvalの設定ファイルを作成する
これまでに作成した処理や、その他設定を行うorval.config.jsを作成する
web/orval.config.js
import { exec } from 'child_process'
export default {
/** @type {import('orval').Options} */
'closed-api': {
input: {
target: '../backend/apps/api-server/src/open-api/open-api.json',
validation: true,
override: {
transformer: 'settings/orval/input-transformer/index.js' // 実行前にOpenAPIを加工するで作成したファイルパス
}
},
output: {
mode: 'tags-split', // タグごとにディレクトリを分けて出力する
target: 'src/api/clients', // Axios, SWR, MSWの出力先
schemas: 'src/api/schemas', // 型定義の出力先
client: 'swr',
mock: {
type: 'msw',
delay: false, // 自動テストで使うので実行が遅くならないようにfalseにしておく
useExamples: true // OpenAPIにexampleがあればfakerではなくexampleの値を使う
},
override: {
useTypeOverInterfaces: true, // interfaceではなくtypeで出力する
mutator: {
path: 'src/lib/axios/customInstance.ts', // customInstanceを作成するで作成したファイルのパス
name: 'customInstance'
},
mock: {
properties: {
'/filter.filters/': [] // MSWのコードで発生してしまう型エラーを防ぐために、特定の値を上書きする
}
}
}
},
hooks: {
afterAllFilesWrite: () => {
exec('npm run post-gen-api-client', (error, stdout, stderr) => {
if (error) {
console.error(`error: ${error}`)
return
}
if (stderr) {
console.error(`stderr: ${stderr}`)
return
}
console.log(`stdout: ${stdout}`)
})
}
}
}
}
web/package.json
// web/package.json
{
...
"scripts": {
...
"gen-api-client": "npx orval",
"post-gen-api-client": "npx ts-node ./settings/orval/hooks/index.ts"
}
...
}
これでOpenAPIが同じならば毎回同じコードが出力される
3. OpenAPI変更の差分検知とコードの自動生成
GitHub Actionsを使用してdevelopブランチにpushされたタイミングで、OpenAPIに差分があったら、自動生成を実行してプルリクエストを作る
developにpushされたタイミングで実行することは下記のメリットがある
- フロントエンドのコードが常にバックエンドの変更に追従した状態に出来る
- developにpush(実質developにmerge)したあとにOpenAPIのJSONを作成するため、コンフリクトが発生しない
- バックエンド開発のプルリクエスト上でOpenAPIのJSON作成を行うとコンフリクトが発生しやすい
- developにマージするまでは特に制限がないので、バックエンドとフロントエンドの修正を分けて行う事ができる
- プルリクエストが作成された時に実行するCIで型チェックやテストが実行されることで、破壊的変更があるかどうかが一目でわかる
GitHub Actionsのワークフロー作成
下記の流れのワークフローを作成する
- OpenAPIが変わりうる場合のみ実行する
- OpenAPIのJSON作成
- 2で作成したファイルに差分があるかチェック
- 差分がなければ終了、あれば継続
- Orvalで自動生成
- プルリクエスト作成
プルリクエストの自動作成にはpeter-evans/create-pull-requestを使用する
下記がポイントになる
- OpenAPI生成時に型チェックが行われてしまうので、型エラーが起きないに必要なコードを生成する
- 適切に権限を付与しないとGitHub Actionsによって作られたプルリクエストのCIが実行されないので、適切な権限を持つgithub appを作成して、create-github-app-tokenなどを使いtokenを取得する
- 2と5のstepsを分けてしまうと、2で作られたJSONが5でに引き継がれないので、同じstepsで実行する
- stepをまたいでファイルを渡すようにしても良いが、同じstepで実行してしまえば特に工夫しなくて良い
create.pr.gen-api-clients.yml
name: Generate API Clients
on:
push:
branches:
- develop
paths:
- backend/** # OpenAPIのJSONが変更される可能性がある場合のみ実行する
- .github/workflows/create.pr.gen-api-clients.yml # debugしやすいようにこのワークフローに変更があった場合も実行する
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true # 連続でdevelopにマージされた時に先に実行されたワークフローを中断する
jobs:
generate-api-clients-and-create-pr:
if: github.actor != 'dependabot[bot]' # dependabotのプルリクエストがマージされた時は実行しない
runs-on: linux-arm64-4
timeout-minutes: 5 # cacheが効いていなくてもだいたい3分弱で終わる
steps:
- name: Generate GITHUB_APPS_TOKEN
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.BOT_APP_ID }}
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .node-version
- name: Install dependencies for backend
working-directory: backend
run: npm ci
- run: npm run prisma:generate # 型エラー回避のためにORMの型を生成
working-directory: backend
- name: Generate OpenApi document
working-directory: backend
run: npm run gen-open-api
- name: Check if there are changes
id: check_if_changes
run: git diff --exit-code || echo "has-changes=true" >> $GITHUB_OUTPUT # 差分チェックを行う
- name: Install dependencies for web-v2
if: ${{ steps.check_if_changes.outputs.has-changes == 'true' }}
working-directory: web-v2
run: npm ci
- name: Generate API Clients
if: ${{ steps.check_if_changes.outputs.has-changes == 'true' }}
working-directory: web-v2
run: npm run gen-api-client
- name: Create Pull Request
if: ${{ steps.check_if_changes.outputs.has-changes == 'true' }}
uses: peter-evans/create-pull-request@v7
with:
token: ${{ steps.generate_token.outputs.token }}
commit-message: 'chore: generate api clients'
title: 'Update api clients'
body: 'This PR is generated by the workflow to update the API clients.'
branch: generate-api-clients
base: develop
labels: 'OpenApi' # わかりやすいようにラベルを付ける。このラベルがついたプルリクエストがオープンになっていたらmainにマージできないようにする、とかもやりやすい
これでフロントエンドがバックエンドの変更に確実に追従できる
backendに差分があるときのみ実行するワークフローが他にある場合、OpenAPIのJSONに差分があった場合は実行しないようにしておくと無駄になCI実行を抑えられて良い
自動生成では対応出来ない点
useSWRMutationを使ってfetchする
POST, PUT, DELETEはuseSWRMutationの関数が生成されるが、GETでは生成されない
そのため下記のようなコードを手動で生成して対応している。keyとfetcherは自動生成されたものを使うので、バックエンドの変更があった場合にも追従できる
エンドポイントのパスパラメーターが動的な場合も、triggerの引数に動的なパスが渡せるので、問題なく使用できる
useSWRMutaionを使ってfetchする
// importしてるコードは自動生成している
import {
getSampleControllerFindOneKey,
useSampleControllerFindOneByMutation
} from 'src/api/clients/sample/sample'
import { type SampleControllerFindOneParams } from 'src/api/schemas'
export const useSampleControllerFindOneByMutation = (id: number) => {
const swrKey = getSampleControllerFindOneKey(id)
const swrFn = (_: unknown, { arg }: { arg: SampleControllerFindOneParams }) =>
sampleControllerFindOne(arg)
const mutation = useSwrMutation(swrKey, swrFn)
return {
...mutation,
swrKey
}
}
MSWのレスポンスを好きな値する
生成されるMSWのコードはfakerでダミーデータを返してくれる。画面が必要とする値をまるっと返すAPIの場合は都度違うデータを返してくれると問題点に気が付きやすいのでメリットが大きいが、複数のエンドポイントを組み合わせて画面を描画する場合は、整合性のあるレスポンスが返ってこないと使い勝手が悪い
- orval.config.jsのoverrides.mock.propertiesで上書きする
- OpenAPIのexampleに整合性のある値を書く
といったことで対応できるが、1は可読性的に厳しく、2は本来望ましいかもしれないが、OpenAPIのためにバックエンドが頑張らないといけないことが多くなりすぎるので、自動生成されたコードをimportして、手動で上書きする層を作り、自動テスト等モックサーバーを利用したい場合はそこからimportするようにした
下記のスクリプトである程度一気に手動で上書きする層のコードは作れたが、必要なコードを過不足なく抽出するのが難しく、不完全なスクリプトなので、手動で修正する箇所が出る可能性がある
エンドポイントが増えた場合にも気軽に使えるので、全て手動で全て書くよりは楽ができる
自動作成されたMSWを手動で上書きする層を作るコード
import { exec } from 'child_process'
import { Project, SourceFile } from 'ts-morph'
const project = new Project({
tsConfigFilePath: 'tsconfig.json'
})
const MOCK_API_DIR = 'src/test-utils/mock/api'
const createMockMswFiles = (sourceFile: SourceFile) => {
const filepath = sourceFile.getBaseName()
return project.createSourceFile(`${MOCK_API_DIR}/${filepath}`, '', {
overwrite: false // すでにファイルが存在していたら上書きしない
})
}
const copyMswMethods = (sourceFile: SourceFile, mockMswFile: SourceFile) => {
const variableDeclarations = sourceFile.getVariableDeclarations()
variableDeclarations.forEach(variableDeclaration => {
const name = variableDeclaration.getName()
const returnType = variableDeclaration.getType().getText()
// この条件で余分な関数をimportしてしまうことがある
if (
name.startsWith('get') &&
name.endsWith('Mock') &&
returnType.includes('[]')
) {
mockMswFile.addStatements(`export const ${variableDeclaration.getText()}`)
mockMswFile.fixMissingImports()
}
})
}
const sourceFiles = project.getSourceFiles(['src/api/clients/**/*.msw.ts'])
sourceFiles.forEach(sourceFile => {
try {
const mockMswFile = createMockMswFiles(sourceFile)
copyMswMethods(sourceFile, mockMswFile)
} catch (error) {
// not overwriting existing files
}
sourceFile.saveSync()
})
project.saveSync()
exec(`eslint --cache --fix '${MOCK_API_DIR}'`)
exec(`npx prettier --write '${MOCK_API_DIR}'`)
ここのコードを上書きする
import {
getSampleControllerCreateMockHandler,
getSampleControllerFindAllMockHandler,
getSampleControllerFindOneMockHandler,
getSampleControllerUpdateMockHandler,
getSampleControllerRemoveMockHandler
} from 'src/api/clients/sample/sample.msw'
export const getSampleMock = () => [
getSampleControllerCreateMockHandler({ message: 'ok' }),
getSampleControllerFindAllMockHandler([
{ id: 1, otherId: 'sampe1', name: 'sample', usesGmail: true },
{ id: 2, otherId: 'sampe2', name: 'sample2', usesGmail: false }
]),
getSampleControllerFindOneMockHandler({
id: 1,
otherId: 'sampe1',
name: 'sample',
usesGmail: true
}),
getSampleControllerUpdateMockHandler(),
getSampleControllerRemoveMockHandler()
]
感想
バックエンドの変更にフロントエンドが確実に追従出来るようにするのが最大の目的だったので、型定義のみ出力できれば、出来ればURLも出力できればやりたいことは満たせましたが、Orvalの幅広いサポートと柔軟性のお陰で、フロントエンドにある、APIと関連を持つほぼ全てのコードを自動で生成出来るので、安心感と作業効率が大幅にアップしました!
OpenAPIを正確に作るという点で少し苦戦しましたが、それさえ出来れば他は簡単でした!
バックエンドとフロントエンドだけならばOpenAPIを経由せずに型定義ファイルをバックエンドとフロントエンドで共有するのでも十分かもしれませんが、OpenAPIが起点になることで、今後バックエンドの言語が変わったり、別のシステムがAPIを使うようになっても何かと対応しやすいので、長い目で見てもプラスに働きそうです!
Discussion