25000行超えのAPIドキュメントを分割した話
はじめに
COUNTERWORKSバックエンドエンジニアの伊藤です。
この記事ではAPIドキュメント分割の知見を紹介します。
弊社では OpenAPI を使用したスキーマ駆動開発を採用しています。
1ファイルで管理していたところ、25000行を超える行数となり管理コストが高くなっていました。
そこで分割作業を実施したのですが、どのような方針でどう対応したかを紹介します。
1ファイルで運用するデメリット
そもそもどんなデメリットが発生していたのかを記載します。
- 全体の構造が把握しづらく、新規参画者への認知負荷が高い
- 行数が多すぎるため、RubyMine など IDE やエディタのパフォーマンスが落ちる
- 1ファイルの内部で複数の箇所を参照しているが、それぞれ
Command f
で該当部分を探す必要がある。そのため、見ているコードの箇所が頻繁に飛んで情報が追いづらい
実際にやったこと
方針
チームでは複数の開発タスクが並行して進んでいるため、分割作業で階層構造が複雑化して説明が必要になるなど、APIドキュメントの編集で必要な情報が増えて他チームの作業を止めたくありませんでした。
そこでフォルダ階層を分かりやすく、階層を増やしすぎたり複雑化させない方針で対応しました。
以下、具体的にどう変えたかを変更の前後で比較して説明します。
before ←→ afterの比較
ファイル、フォルダ構成
before
├── openapi.yml # 1ファイルに定義を全て記載しており、肥大化している
└── src/openapi/api.ts # 上記の定義から自動生成される型情報
after
├── openapi
│ ├── root.yml # 参照元のファイル
│ ├── components/schemas/xxx.yml # 参照先のファイル
│ └── paths/xxx.yml # 参照先のファイル
└── src/openapi/api.ts
元々のopenapi.yml
をopenapi/root.yml
と名前を変更して、
openapi
以下にpathsやschemas用のフォルダを分けることに集中しました。
過剰なファイル階層のネストや、今まで見慣れていない名称のフォルダができると認知負荷が高くなると考え、できるだけ分かりやすい形で増やすフォルダ数を抑えて対応しました。
各ファイルの関係性
-
root.yml
(参照元となるAPIドキュメント全体の定義)
↓$refs:
で参照 -
paths
以下のファイル(GET, POSTなどのHTTPメソッドに対する定義)
↓$refs:
で参照 -
schemas
以下のファイル(取り扱うリソースについての定義)
今回の例では/g/users
というリクエストを送り、ユーザーの一覧情報を取得してレスポンスを返す処理について、APIドキュメントの定義を想定します。
参照元となるAPIドキュメント全体の定義
before
# openapi.yml
openapi: "3.0.0"
info:
version: 1.0.0
servers:
- url: 'http://localhost:3000'
description: "ローカル環境"
paths:
/g/users:
get:
summary: "ユーザー一覧取得"
operationId: users.index
tags:
- Users
security:
- Bearer: []
parameters:
- name: name
in: query
description: 名前
style: form
required: false
schema:
type: string
- name: email
in: query
description: メールアドレス
style: form
required: false
schema:
type: string
responses:
'200':
description: "成功"
content:
application/json:
schema:
type: object
required:
- users
properties:
users:
type: array
items:
allOf:
- ref: '#/components/schemas/User'
# 中略
'401':
description: "認証失敗"
'422':
description: "バリデーションエラー"
# (中略、同様の定義が続く)
components:
schemas:
User:
type: object
required:
- id
- name
- email
- created_at
- updated_at
description: ユーザー
properties:
id:
$ref: '#/components/schemas/User_Id'
name:
$ref: '#/components/schemas/User_Name'
email:
$ref: '#/components/schemas/User_Email'
created_at:
$ref: '#/components/schemas/DateTime'
updated_at:
$ref: '#/components/schemas/DateTime'
User_Id:
type: integer
description: ユーザーID
User_Name:
type: string
description: ユーザー名
User_Email:
type: string
description: メールアドレス
# (中略、同様の定義が続く)
DateTime:
type: string
format: date-time
example: '2024-03-01T12:00:00+09:00'
# (以下略)
1つのファイルでpaths、schemas
の情報すべてを扱っているため、定義が増えて行くにつれ情報を探すのが大変になります。
またファイルの行数が増える度に、全体の構造を掴むのが難しくなります。
after
# openapi/root.yml
openapi: "3.0.0"
info:
version: 1.0.0
servers:
- url: 'http://localhost:3000'
description: "ローカル環境"
paths:
/g/users:
get:
$ref: "paths/users.yml" # GET
# (中略、同様の定義が続く)
components:
schemas:
DateTime:
type: string
format: date-time
example: '2024-03-01T12:00:00+09:00'
# (以下略)
$ref: "paths/users.yml" # GET
の部分はドワンゴさんの記事を参考に、分割先のpaths
のファイルに対して、どのHTTPメソッドが対応しているか明記しました。
分割したpathsのファイル
# openapi/paths/users.yml
get:
summary: "ユーザー一覧取得"
operationId: users.index
tags:
- Users
security:
- Bearer: []
parameters:
- name: name
in: query
description: 名前
style: form
required: false
schema:
type: string
- name: email
in: query
description: メールアドレス
style: form
required: false
schema:
type: string
responses:
'200':
description: "成功"
content:
application/json:
schema:
type: object
required:
- users
properties:
users:
type: array
items:
allOf:
- ref: '../components/schemas/users.yml#/User'
# 中略
'401':
description: "認証失敗"
'422':
description: "バリデーションエラー"
分割したschemasのファイル
# openapi/schemas/user.yml
User:
type: object
required:
- id
- name
- email
- created_at
- updated_at
description: ユーザー
properties:
id:
$ref: '#/User_Id'
name:
$ref: '#/User_Name'
email:
$ref: '#/User_Email'
created_at:
$ref: '../../root.yml#/components/schemas/DateTime'
updated_at:
$ref: '../../root.yml#/components/schemas/DateTime'
User_Id:
type: integer
description: ユーザーID
User_Name:
type: string
description: ユーザー名
User_Email:
type: string
description: メールアドレス
openapi
フォルダの中で、root.yml
からpaths
やschemas
に対応するファイルを参照する形でファイルを分けることが出来ました。
作業していく中で苦労したこと
また作業を進める上で、以下の点が特に大変でした。
- 型情報生成コマンドのエラー内容が読み取りづらい(情報と記載が間違っている箇所が1対1でない)
- 上記エラー内容から記載が誤っている箇所を特定しづらいため、一度に大きな変更を行ってエラーが発生した場合に、原因の特定、修正が難しい
- 並行している開発タスクとコンフリクトが発生し、それを解消する作業が頻繁に発生した
これらの問題は分割するファイルを小さい単位にして、エラー発生時に原因の特定がしやすい状況で作業を進めました。
またコンフリクトには並行作業のAPIドキュメントに対するコミットログを1つずつ確認して、分割後のコードに変更分を反映させて対応しました。
実際やってみてどうなったか?
以上の方針で対応した結果、ファイル行数を半分近く減らすことが出来ました!
分割したことで、開発時の PullRequest でAPIドキュメントの差分が見やすくなり、1ファイルでスクロールして確認する必要が無くなりました。
ヒアリング結果
以下得られたメリットやデメリットを元に、分割作業に賛成反対の意見をヒアリングしました。
具体的なpros / cons
pros
- APIドキュメントの構造を把握しやすくなり、修正箇所が分かりやすくなった
- エディタやIDEが固まらなくなり、ファイル表示が速くなった
- テーブル名でファイルを開くことにより、コンポーネントの定義場所へのアクセスが楽になった
- パスに対応するHTTPメソッドを別ファイルで一覧表示でき、APIの挙動確認がしやすくなった
cons
- 並行作業のAPIドキュメントと差分がある場合、コンフリクトの解消が必要になった
- 一部分割せず保留した部分があり、元ファイルに残っているか分割されたか確認する作業が発生
膨大な行数のファイルを分割した結果、API定義部分へのアクセスや、エディタでファイル編集がしやすくなり作業効率が大きく改善されたという意見が多かったです。
今後の展望や課題
今後は保留にした部分(型情報に変更が発生する)の分割対応をしたいと考えています。
また他プロダクトでも分割のノウハウを活かして開発効率の向上に貢献できれば嬉しいです。
終わりに
弊社では新規開発を進めながら、技術的負債に向き合ってリファクタリングを実施しています。
コード品質や開発効率にこだわりを持ったエンジニアの方は、ぜひカジュアル面談でお話ししましょう!
ポップアップストアや催事イベント向けの商業スペースを簡単に予約できる「SHOPCOUNTER」と商業施設向けリーシングDXシステム「SHOPCOUNTER Enterprise」を運営しています。エンジニア採用強化中ですので、興味ある方はお気軽にご連絡ください! counterworks.co.jp/
Discussion