📑

25000行超えのAPIドキュメントを分割した話

2024/03/22に公開

はじめに

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.ymlopenapi/root.ymlと名前を変更して、
openapi以下にpathsschemas用のフォルダを分けることに集中しました。

過剰なファイル階層のネストや、今まで見慣れていない名称のフォルダができると認知負荷が高くなると考え、できるだけ分かりやすい形で増やすフォルダ数を抑えて対応しました。

各ファイルの関係性

  • 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メソッドが対応しているか明記しました。
https://blog.nnn.dev/entry/2022/04/20/110000

分割した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からpathsschemasに対応するファイルを参照する形でファイルを分けることが出来ました。

作業していく中で苦労したこと

また作業を進める上で、以下の点が特に大変でした。

  • 型情報生成コマンドのエラー内容が読み取りづらい(情報と記載が間違っている箇所が1対1でない)
  • 上記エラー内容から記載が誤っている箇所を特定しづらいため、一度に大きな変更を行ってエラーが発生した場合に、原因の特定、修正が難しい
  • 並行している開発タスクとコンフリクトが発生し、それを解消する作業が頻繁に発生した

これらの問題は分割するファイルを小さい単位にして、エラー発生時に原因の特定がしやすい状況で作業を進めました。
またコンフリクトには並行作業のAPIドキュメントに対するコミットログを1つずつ確認して、分割後のコードに変更分を反映させて対応しました。

実際やってみてどうなったか?

以上の方針で対応した結果、ファイル行数を半分近く減らすことが出来ました!

分割したことで、開発時の PullRequest でAPIドキュメントの差分が見やすくなり、1ファイルでスクロールして確認する必要が無くなりました。

ヒアリング結果

以下得られたメリットやデメリットを元に、分割作業に賛成反対の意見をヒアリングしました。

具体的なpros / cons

pros

  • APIドキュメントの構造を把握しやすくなり、修正箇所が分かりやすくなった
  • エディタやIDEが固まらなくなり、ファイル表示が速くなった
  • テーブル名でファイルを開くことにより、コンポーネントの定義場所へのアクセスが楽になった
  • パスに対応するHTTPメソッドを別ファイルで一覧表示でき、APIの挙動確認がしやすくなった

cons

  • 並行作業のAPIドキュメントと差分がある場合、コンフリクトの解消が必要になった
  • 一部分割せず保留した部分があり、元ファイルに残っているか分割されたか確認する作業が発生

膨大な行数のファイルを分割した結果、API定義部分へのアクセスや、エディタでファイル編集がしやすくなり作業効率が大きく改善されたという意見が多かったです。

今後の展望や課題

今後は保留にした部分(型情報に変更が発生する)の分割対応をしたいと考えています。
また他プロダクトでも分割のノウハウを活かして開発効率の向上に貢献できれば嬉しいです。

終わりに

弊社では新規開発を進めながら、技術的負債に向き合ってリファクタリングを実施しています。
コード品質や開発効率にこだわりを持ったエンジニアの方は、ぜひカジュアル面談でお話ししましょう!
https://herp.careers/v1/counterworks/YSV0OTzvTTDa

COUNTERWORKS テックブログ

Discussion