はじめに
フロントエンドとバックエンドを分離して開発する体制は広く使われています。その際、最初にSwaggerでAPI仕様を決めてから開発を進めるやり方も一般的です。
やってみて気づいたのは、この進め方には「後から設計を変えにくい」という落とし穴があるということです。
この記事では、実際に困ったこと、なぜそうなるのか、どう対処すればよかったかをまとめます。
やっていた開発の進め方
それまでの自分はフルスタック開発しか経験がなく、APIも画面もその都度一人で作る進め方に慣れていました。必要になったら作る、というアジャイル的なサイクルが自分には合っていました。
そのため、フロントとバックを分離してSwaggerで先に仕様を固めるやり方は、最初は新鮮に映りました。しかし実際に進めてみると、一人でフルスタック開発するときには起きなかった問題が出てきました。
チームはフロントエンドとバックエンドで席を分けて作業していました。開発の流れは次のとおりです。
- 最初にSwaggerでAPIのエンドポイント・リクエスト・レスポンスを決める
- フロントとバックがそれぞれ並行して実装する
- 最終的に結合してテストする
一見、効率的に見えます。しかし実装を進めるうちに問題が出てきました。
実際に起きた問題
空文字とnullを途中で変えられなかった
最初、「未入力は空文字("")で返す」仕様でAPIを実装していました。
しかし実装を進めるうちに、未設定は null で返す方が設計として正しいと気づきました。このとき、APIの互換性を壊さずに変更することができず詰まりました。
フロントはすでに空文字前提でバリデーションを組んでいたため、nullに変えるとフロント側の修正も発生します。調整のために改めて確認・合意をとる工数がかかり、結局当初の仕様のままで進めることになりました。
null・空文字・キー省略の使い分けについては、JSONレスポンスのnull・省略・空文字を混在させてはいけない で詳しく整理しています。今回の問題は、この設計方針を最初に固めないままSwaggerで仕様合意してしまったことでもありました。
フィールド名やレスポンス構造を変えたくなった
実装を進めると、次のように感じる場面が出てきます。
- このフィールド名はわかりにくい
- ここはネストにした方がいい
しかしSwaggerで合意したあとの変更は、フロント・バック双方に影響するため気軽に提案しにくい状況でした。
例えば次のような変更は、どれも破壊的変更になります。
-
user_nameをusernameに変える - フラットなレスポンスを
{ "user": { ... } }にネストする - 配列で返していたものを
{ "items": [...], "total": n }に変える
これらは命名や構造として自然な変更ですが、フロントがすでに実装済みの場合は修正が必要になります。席が遠い場合は特にそうで、ちょっとした相談でもSlackや会議が必要になり、コストが高く感じられました。結果として「まあこのままでいいか」という判断を繰り返してしまいました。
なぜこうなったか
問題の根本は次の3つです。
- 契約(API仕様)を早い段階で固定しすぎた
- フロントとバックの検証サイクルが分断されていた
- スキーマ設計を検証する前に合意してしまっていた
最初に決めたSwaggerが「完成版」として扱われてしまうため、変更の心理的コストが高くなります。実装中に得られる知見は多いですが、それをフィードバックして設計を修正するサイクルが回りませんでした。
ウォーターフォール的な前提との相性
Swagger先行で仕様を固めるやり方は、ウォーターフォール的な進め方に近くなります。
この進め方自体が悪いわけではありませんが、成立させるには前提条件があります。
- API設計のベストプラクティスがチーム内で共有されている
- null / 空文字 / 省略などの扱いが事前に定義されている
- ある程度、最終形のスキーマが見えている
これらが揃っていない状態で最初に仕様を固定すると、後からの変更が難しくなり、結果として設計が硬直化します。
実際には、実装して初めて気づくことも多く、最初から最適なAPI設計を出すのは簡単ではありません。
気づくのは開発が終わってからが多い
問題に気づくタイミングとして多いのは、開発が一段落してレビューや振り返りの時間ができたときです。
- このAPIのインタフェース設計はおかしかった
- テーブル構造にミスがあった
- フィールド名の命名が一貫していない
開発中は実装を進めることに集中しているため、設計の問題に気づきにくい状態になりがちです。余裕が生まれたタイミングで初めて全体を俯瞰でき、修正したい箇所が見えてくることが多くあります。
しかし仕様が固定されたあとでは、インタフェースの変更もテーブルの修正も、フロント・バック双方への影響が大きく、手を入れにくくなっています。気づいたときには修正コストが高くなっているというのが、Swagger先行開発の難しさの一つです。
こうすればよかったと思うこと
最初のSwaggerを暫定版として扱う
最初から完璧な仕様を作ろうとしないことが重要です。
「暫定版」「v0」といったラベルをつけて、変更があることを前提にしておきます。これだけでも変更提案の心理的ハードルが下がります。
モックサーバーで早めに結合する
最終的な結合テストを待たず、モックサーバーを使ってフロントとバックを早めにつなぎます。
実際にデータをやりとりすることで、スキーマの問題を早い段階で発見できます。
フロント・バックで一緒に設計する
Swaggerの設計フェーズにフロントとバックが同席してレビューします。
後から気づいた問題も「設計を見直す場」があれば相談しやすくなります。定期的なAPI設計レビューを設けるだけでも効果があります。
破壊的変更を許容するフェーズを設ける
開発初期は「API仕様は変わっていい」フェーズとして明示的に設定します。
例えば結合テスト開始前までは破壊的変更を許容し、結合テスト以降で仕様を凍結するという区切りを作るだけでも、実装中の改善を取り込みやすくなります。
なお、リリース後にAPIを変更する場合は /v2/users のようにバージョニングで対応するのが一般的です。ただしそれはリリース後の話であり、開発中の設計改善はバージョニングではなく「変更可能なフェーズを設けること」で対処するのが現実的です。
まとめ
Swagger先行開発は、フロントとバックの並行作業を可能にする点で有効です。一方で、API仕様を早期に固定することで設計の改善機会を失うリスクがあります。
対策は次の3つです。
- 最初の仕様を暫定版として扱う
- 早めに結合して検証する
- 設計レビューの場を設ける
完璧なSwaggerを最初から作ろうとするより、変更を受け入れる仕組みを作る方が、最終的な設計の質が上がると感じています。
そもそも、ベストな設計は最初から見えているわけではありません。実装を進める中で初めて気づくことが多く、そこで修正できる余地を残せるかどうかが、設計の質を左右します。Swagger先行開発の難しさは、その余地を早い段階で閉じてしまいやすい点にあります。