API開発におけるスキーマ駆動開発採用の経緯
はじめに
はじめまして!
株式会社SUPERSTUDIOでエンジニアをしています、@fqqk と申します。
弊社では ecforceという統合コマースプラットフォームを提供しています。
今回はAPI開発における開発プロセスの改善に関するお話です。
私の所属するチームでは、ecforceの一部ドメインの切り離しとして、新規APIの開発プロジェクトを進めています。
外部公開APIとしてAPIドキュメントは必ずと言っていいほど必要になるかと思います。
当初私たちは、rspec-openapiを使用してテストコードからAPIドキュメントを自動生成していました。
しかし、運用上の課題に直面し、OpenAPIスキーマ駆動開発へと開発手法を転換しました。
本記事では、その転換の経緯と新しい開発プロセスの採用についてご紹介します。
本題
目次
開発フローの変更概要
以下の図は、今回の開発手法転換の全体像を示しています。
従来のテストコードからAPIドキュメントを自動生成するアプローチでは、手作業による修正が必要な箇所で多くの問題が発生していました。一方、OpenAPIスキーマ駆動開発では、最初にスキーマを定義することで、これらの問題を根本的に解決しています。
1.従来の開発フローとその課題
従来の新規API開発の流れ
これまでの新規APIの開発では、以下のような流れで進めていました。
- 仕様を固める
- 仕様に沿ってコーディング
- テストコードを書く
- テストコードからAPIドキュメント用のyamlファイルを生成する
この流れは一見効率的に見えますが、実際の運用で複数の課題が浮き彫りになりました。
[補足]OpenAPI Specificationとは
OpenAPI Specification(OAS)は、HTTP APIのインタフェースを定義するための仕様で広く採用されています。OASに則ってAPIのインタフェースを記述することにより、人間とコンピュータ双方がAPIの利用方法を理解できます。現在はOpenAPI Initiativeという組織で管理、推進されています。
2.手作業が発生する自動化の問題
大変だったこと
1. 手作業での修正時のレビューコストが大きい
- 手作業の際、修正すべき箇所を探すために、自動化で生成された膨大な箇所のレビューが必要となります。
- 内容理解が十分ではない状態でメンテされてしまうと、ドキュメントの一貫性の欠如につながる可能性があります。
2. テストデータとドキュメントデータの不整合
- テストデータの内容が必ずしもドキュメントに掲載したい情報とは限らない。
- テストデータのプロパティは一貫性がないため、それに応じてドキュメント側の期待値も変わり、結果としてコード差分が発生。コンフリクトの温床となっていた。
3. テストコードの可読性への影響
- テストコードにAPIドキュメントに反映するための記述が膨大になってしまうと、テストコードの可読性、保守性の低下に繋がる
上記3点から、手作業が発生する自動化ではAPIドキュメントを無理なくリリース水準に保ち続けることは厳しいという判断をしました。
リリース水準のドキュメントに必要な要素
我々のリリース水準のAPIドキュメントには以下の記載を必要としていました。
- 項目の説明
- code example(reactの場合の書き方、curlの場合の書き方等)
- 必要に応じて画像による説明
- markdownのテーブルでの説明
これらをすべてテストコードに記載することを試みた際、テストコードの可読性の低下と、テストコードの本来の役割を逸脱しかねないという懸念が拭えず、転換を決断する大きな理由となりました。
上記項目を記載した場合のテストコードの例
RSpec.describe 'GET /v1/products', type: :request do
# describeの記法やitの記法もAPIドキュメントに反映させることを意識する必要があった。
describe '商品一覧取得' do
# テストで使用する商品データを作成
let!(:product) { create(:product, name: '高機能マットレス') }
let(:products) { [product] }
it '商品一覧を取得できること',
# --- ここからがドキュメント生成のためのメタデータ ---
summary: '商品一覧の取得',
description: <<-DESC
公開中で販売可能な商品の一覧を返します。
ページネーションやソート順、キーワードによる絞り込みが可能です。
### パラメータ仕様
| パラメータ名 | 型 | 説明 |
| :--- | :--- | :--- |
| `page` | `integer` | ページ番号を指定します。 |
| `per` | `integer` | 1ページあたりの表示件数を指定します。 |
| `q[s]` | `string` | ソート順を指定します。(例: `released_at desc`) |
### 絞り込み条件の例
`q[name_cont]=マットレス&q[sales_price_gteq]=10000`

DESC do
# --- メタデータここまで ---
get('/v1/products', headers:)
aggregate_failures 'product_list spec' do
expect(response).to have_http_status(:ok)
response_json = JSON.parse(response.body)
expect(response_json['total_count']).to eq 1
product_json = response_json['products'][0]
expect(product_json['name']).to eq '高機能マットレス'
expect(product_json['is_new']).to be true
expect(product_json['is_sale']).to be true
expect(product_json['is_sold_out']).to be false
end
end
end
end
3.OpenAPIスキーマ駆動開発への転換
スキーマ駆動開発とは
API 仕様を記述するスキーマ(契約)を最初に定義し、それを中心にシステム開発を進めるアプローチです。
API仕様を最初に定義することで、我々のプロジェクトでは「開発速度・品質」においてメリットを享受することができました。
転換を決定した背景
1. 手戻りの削減による開発速度の向上
- 事前にschemaの内容の合意形成を行うことで、試験時にパラメータの名称の議論不足を指摘されることが減ったことと、リリース内容への納得感が一層高まりました。
2. committee-railsでの検証効果の最大化による品質の向上
- API仕様書と実際の挙動がズレるのを防ぐ仕組みを効果的に活用
3. AIとの親和性
ちょうど社内でAIの活用が進んでいたため、schemaファイルの手動作成自体をAIに任せられるという確証がありました。
- cursorを活用し、openapi schemaファイル自体をAPI仕様から生成
- 生成されたopenapi schemaファイルを元に意図したアプリケーションコードとテストコードの生成を自動化
4.新しい開発フローの確立
新しい開発の進め方
①プロダクトバックログリファインメントにてAPI仕様を決定
弊社では、PdM、エンジニア、QAエンジニアが揃ってリクエスト・レスポンス仕様の議論を行います。
追加調査の必要性等はこの段階で洗い出すことができます。
②エンジニアが担当エンドポイントのopenapi仕様書作成タスクを進める
エンジニア内でPRレビューを行います。
ここで開発担当エンジニア以外が気になった箇所は、以降のスプリントレビュー時にスクラムでの議題として取り扱っています。
このフローを事前に挟むことで、実装タスクのレビュー時にリクエスト・レスポンス仕様について言及する必要がなくなったことが大きなメリットです。
実装タスクが終わってからschema修正の指摘が入り、実装自体の見直しが発生するというケースも排除できています。
③schemaレビューをする(PDM含む)
スプリントレビューにて、開発チームによって承認されたopenapi仕様書を 「ビルド or 何かしら閲覧できる状態」 にしてレビューします。
この段階で、開発担当エンジニアの調査で判明したことを元に、リクエスト、レスポンスのパラメータ名の見直しやエンドポイント名の見直し等が行われます。
開発担当エンジニアはこの段階をクリアして初めて、実装タスクに着手できます。
④コーディング
このフローも現在、AIでできるように整備中です。
④テストコード
committee-railsを使用してAPIスキーマ定義に基づいて、リクエストとレスポンスの自動検証をしています。
5.我々のopenapi.yamlの構成について
本項目では、実際に我々が採用しているOpenAPIファイルの管理方法について説明します。
ディレクトリ構成
ファイル分割の方針は以下の通りです。
- 単一責任: 各ファイルは一つの責任を持つ
-
再利用性: 共通部分は
$ref
で参照 - 並行開発: エンドポイント毎にファイルが分かれているため競合しにくい
📁 プロジェクトルート/
├── 📄 openapi.yaml # 最終的なAPIドキュメント(全ファイルの統合結果)
├── 📄 docs/template.hbs # redoc-cli用のfavicon設定テンプレート
└── 📁 docs/openapi/ # OpenAPI定義ファイル群
├── 📄 info.yaml # API基本情報(タイトル、バージョン等)
├── 📄 servers.yaml # サーバー情報
├── 📄 tags.yaml # エンドポイントのタグ定義
├── 📄 tag_groups.yaml # タググループ(ドキュメント表示用)
└── 📁 components/ # 再利用可能なコンポーネント群
├── 📄 security_schemas.yaml # 認証スキーマ定義
├── 📁 parameters/ # パラメータ定義
│ ├── 📁 path/
│ ├── 📁 query/
│ └── 📁 header/
├── 📁 paths/ # APIエンドポイント定義
│ ├── 📁 customer/
│ │ ├── 📄 get.yaml
│ │ ├── 📄 post.yaml
│ │ ├── 📄 put.yaml
│ │ └── 📁 card/
│ │ └── 📄 index.yaml
│ └── 📁 product/
│ ├── 📄 get.yaml
│ ├── 📄 index.yaml
│ └── 📁 reviews/
│ └── 📄 index.yaml
├── 📁 schemas/ # データ構造定義
│ ├── 📁 customer/
│ └── 📁 product/
└── 📁 examples/ # APIレスポンス例
├── 📁 customer/
└── 📁 product/
ファイル管理の方針
1. メインファイル(openapi.yaml)の構成
最終的なopenapi.yaml
では、各部分を$ref
で参照して構築しています。
命名規則:
-
一覧系:
./openapi/paths/[リソース名]/index.yaml#/index
-
単体系:
./openapi/paths/[リソース名]/[HTTPメソッド].yaml#/[HTTPメソッド]
openapi: 3.0.3
info:
$ref: './openapi/info.yaml'
servers:
$ref: './openapi/servers.yaml'
tags:
$ref: './openapi/tags.yaml#/tags'
paths:
/v1/products:
get:
$ref: './openapi/paths/product/index.yaml#/index'
/v1/product:
get:
$ref: './openapi/paths/product/get.yaml#/get'
post:
$ref: './openapi/paths/product/post.yaml#/post'
put:
$ref: './openapi/paths/product/put.yaml#/put'
delete:
$ref: './openapi/paths/product/delete.yaml#/delete'
components:
securitySchemes:
AccessToken-Bearer-Auth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
このAPIはアクセストークンによる認証を必要とします。
Bearer認証を用いる場合、リクエストヘッダー `Authorization`に下記の形式でアクセストークンを指定します。
```
Authorization: Bearer␣{アクセストークン}
```
x-tagGroups:
$ref: './openapi/tag_groups.yaml'
2. 各エンドポイントファイルの構造
エンドポイント定義ファイルは、HTTPメソッド名をキーとしたシンプルな構造です。
# paths/product/get.yaml
get:
summary: "商品詳細取得"
description: "指定されたIDの商品詳細情報を取得します"
parameters:
- $ref: '../../../components/parameters/path/product_id.yaml#/ProductId'
responses:
'200':
$ref: '../../../components/responses/product/get.yaml#/ProductGetResponse'
# paths/product/index.yaml
index:
summary: "商品一覧取得"
description: "公開中の商品一覧を取得します"
parameters:
- $ref: '../../../components/parameters/query/pagination.yaml#/Page'
- $ref: '../../../components/parameters/query/pagination.yaml#/PerPage'
responses:
'200':
$ref: '../../../components/responses/product/index.yaml#/ProductIndexResponse'
3. コンポーネントの参照パターン
再利用可能な要素は、components配下で管理し、$ref
で参照します。
# パラメータの参照例
parameters:
- $ref: '../../../components/parameters/header/auth.yaml#/CustomerAccessToken'
- $ref: '../../../components/parameters/path/customer_id.yaml#/CustomerId'
# レスポンスの参照例
responses:
'200':
content:
application/json:
schema:
$ref: '../../../components/schemas/customer/get.yaml#/CustomerGetResponse'
examples:
success:
$ref: '../../../components/examples/customer/get.yaml#/CustomerGetSuccess'
4. 各componentsファイルの構造パターン
全てのcomponentsファイルは、OpenAPI標準のcomponents
キーから始まる構造で統一しています。
# schemas/customer/get.yaml
components:
schemas:
CustomerGetResponse:
type: object
properties:
id:
type: string
description: "顧客ID"
name:
type: string
description: "顧客名"
email:
type: string
format: email
description: "メールアドレス"
# examples/customer/get.yaml
components:
examples:
CustomerGetSuccess:
summary: "顧客取得成功例"
value:
id: "cust_123456789"
name: "田中太郎"
email: "tanaka@example.com"
# parameters/path/customer_id.yaml
components:
parameters:
CustomerId:
name: customer_id
in: path
required: true
schema:
type: string
pattern: '^cust_[a-zA-Z0-9]{9}$'
description: "顧客ID(cust_で始まる9桁の英数字)"
まとめ
今回は、テストコードによるAPIドキュメントの自動生成からOpenAPIスキーマ駆動開発への転換についてご紹介しました。
今回の転換により、チームとして以下の成果を得ることができました。
- 開発速度の向上: 事前のスキーマ定義により手戻りを削減
- 保守性の向上: テストコードの可読性の維持とAPIドキュメントの一貫性の確保
OpenAPIスキーマ駆動開発の実現は、メンバーの自律性やプロダクト思考の高さによって支えられている部分が大きいと感じています。
今後もプロダクトの価値向上を目的に、より効率的かつ高品質なAPI開発を実現していきたいと思います。
SUPER STUDIOの採用について
SUPER STUDIOでは、エンジニアを採用しています。
少しでも興味がありましたら、以下をご覧ください。
下記の記事は、SUPER STUDIOのキックオフイベントで表彰されたエンジニアのインタビュー記事です。
SUPER STUDIOのエンジニア組織をより理解できる内容となっておりますので、ご一読ください。
Discussion