物流エンジニアが本気出して考えたAPI設計ルール
※このエントリは、Dev communityにアップした内容を日本語にしました。(元記事の執筆者も私です)
API設計ポリシーについて
APIの設計をする時、どのような設計ポリシーを定義されているでしょうか。
これらには、絶対的な正解はありません。
どのような設計だと分かりやすいのか、応用がききやすいのか、技術負債を抱えずに済むのか、とても頭を悩ませる部分が多いのではないかと思います。
私も、今回のプロジェクトにおいて、どのようなポリシーでAPIを設計するか、とても悩みました。
非常に苦労をしましたが、その甲斐あって、なかなか良いものができたのではないかと自負しております。
それらのルールについては、スペイン在住の友人に話してみると、彼女はとても興味を持ち、「分かりやすい!それ、詳しく知りたいからブログに書いてよ!」と言ってくれました。
なので、今回私が考えた、API設計ポリシーについて、体系化した内容としてまとめることにしました。
この記事を書くにあたり、スペイン在住のエンジニア、R には、多大な協力を頂きました。
私は英文を書くのがあまり得意ではありません。彼女からの的確なアドバイスおよび添削は、とても助けになりました。
この場を借りて、改めて R に謝辞を述べたいと思います。
ちなみに、タイトルは「物流エンジニアが本気出して考えたAPI設計ルール」となっていますが、
物流に特化した内容というわけではなく、Webシステム全般において適用できます。
(私のメイン領域はバックエンドですが、自己紹介をする時は、"物流エンジニア" と名乗る事もあります。その方が、相手の印象に残りやすいケースがあるためです。)
なお、今回ご説明させて頂くのは、「API定義書」ではなく、「API設計書を作成する時の方針」をまとめたものとなります。
イントロダクション
私が今回考えたAPI設計のルールは以下のようなものです。
いくつかのルールは、少し奇妙に見えるかもしれません。
しかし、それらには明確な理由があります。
それらについても、説明していきたいと思います。
なお、今回のプロジェクトでは、バックエンドに Laravel を使用しています。
そのため、一部のルールでは、Laravel や PHP の影響を受けている箇所があります。
それらについては、ルールの再定義が必要な箇所がありますので、それらについても解説していきます。
API ルール
URL 命名規則
名称は、キャメルケースとします。
例:
/webApi/comboItem
URI 設計
URIの設計ルールは以下とします。
コントローラ名は単数形とします。(Laravel ベストプラクティスに準拠)
この表は、Laravel の公式資料のActions Handled By Resource Controller基づいています。
(その資料では、DELETEメソッドに対応するアクション名を "destroy" としていますが、個人的には "delete" の方が分かりやすいのではないかと考えました。)
役割 | HTTPメソッド | URI | アクション(コントローラ層におけるメソッド名) |
---|---|---|---|
一覧表示 | GET | /コントローラ名/index | index |
1件表示 | GET | /コントローラ名/show | show |
新規作成 | POST | /コントローラ名/store | store |
更新 | PUT | /コントローラ名/update | update |
削除 | DELETE | /コントローラ名/delete | delete |
基本思想
URL基本ルール
(操作対象のオブジェクト) / (操作名 ※動詞)
末尾の URL は動詞とします。
また、特別な理由が無い限り、この名称とコントローラのメソッド名と一致させてください。
操作名ルール
操作名を省略していません。そうする事で、URLの階層を統一化しています。
( よく見かける「product/{id} 」といった URL 構成の場合、「参照する」という操作名が暗黙の了解となっている。本プロジェクトにおいては、極力暗黙の了解を作らず、「参照する」という操作を明示するようにしました。
その他ルール
親子関係のあるデータで、子の内容を取得したい場合、親の階層の下に子のモデルを記述します。
※アクション名において、コントローラ以下の階層を想定した連結は「 _(アンダーバー) 」を使用してください。(コーディング規約にて、メソッド名はキャメルケースを使用するルールにしていますが、この場合は例外とします)
例) 親:comboItem, 子:item
役割 | HTTPメソッド | URI | アクション |
---|---|---|---|
一覧表示 | GET | /comboItem/item/index | item_index |
1件表示 | GET | /comboItem/item/show | item_show |
routing example (Laravel 8)
Route::get('comboItem/index', [\App\Http\Controllers\Api\ComboItemController::class, 'index']);
Route::get('comboItem/show' , [\App\Http\Controllers\Api\ComboItemController::class, 'show']);
Route::get('comboItem/item/index', [\App\Http\Controllers\Api\ComboItemController::class, 'item_index']);
Route::get('comboItem/item/show' , [\App\Http\Controllers\Api\ComboItemController::class, 'item_show']);
どうでしょう。いくつかのルールは、不思議に思われたかもしれません。
これらのルールを定義した理由について、1つずつ解説をしていきます。
URLの末尾は、必ず動詞にする
まずは、以下のルールについてです。
(操作対象のオブジェクト) / (操作名 ※動詞)
URI の末尾は、必ず動詞としています。
そして、動詞の直前の階層は、操作対象のオブジェクトを表しています。
そうする事で、「対象のオブジェクトに、どういった操作をするか」という事が、URI を見るだけで判別できるようにしています。
HTTPメソッドの差異のみで、APIの挙動を判別させない
また、HTTPメソッドのみで、APIの挙動を判別する、という事をしていません。
例えば、こういったAPI定義書を目にする事があるかと思います。
今回のルールを適用すると、以下のようになります。
HTTPメソッドにて振る舞いを判別できるので、わざわざ URI に、操作名を入れる必要はない、と考えるかもしれません。
しかし、APIで表現したい事に対して、HTTPメソッドの種類は、あまりにも少なすぎます。
上記の例では、データの参照・更新・削除、といった、ごく簡単な操作しか表現していませんが、実際の APIは、もっと複雑な操作が必要となるでしょう。
例えば、「特定の状態の商品を抽出する」「カテゴリ単位でサマリーした商品の数量を取得する」といった操作をしたい場合、HTTPメソッドでAPIの挙動を表現するのは不可能でしょう。
その操作を表すための動詞がURLに必要となります。
そうすると、どうなるでしょうか。
"HTTP メソッドのみで振る舞いが予測可能なため、URI に動詞が存在しないパターン" と
"HTTP メソッドのみで振る舞いが予測不可能なため、URI に動詞が存在するパターン" の
2種類が存在する事になります。
API設計のルールを定義するにあたり、どちらかに統一化しておきたいと考えました
API設計書全体の見通しの問題だけではなく、設計者によって「この機能の振る舞いは HTTP メソッドで判別し、この機能の振る舞いは URIに操作名を記述する」といった判断に、頭を悩ませて欲しく無かったのです。
そのため、今回のルールでは、HTTPメソッドによってある程度はAPIの挙動が予測可能でも、必ず明示するようにしました。
そこで提案したいのが、下記のルールです。
役割 | HTTPメソッド | URI | アクション(コントローラ層におけるメソッド名) |
---|---|---|---|
一覧表示 | GET | /コントローラ名/index | index |
1件表示 | GET | /コントローラ名/show | show |
新規作成 | POST | /コントローラ名/store | store |
更新 | PUT | /コントローラ名/update | update |
削除 | DELETE | /コントローラ名/delete | delete |
削除処理については、HTTPメソッド名と操作名が重複しているので、違和感を感じる方がいるかもしれません。
しかし、こうする事で URI の階層を統一化できて見通しがよくなるというメリットがあります。
また、API ドキュメントを読まずとも、URI を見るだけで、ある程度の挙動が予測可能な状態となります。
コントローラ層のメソッドと一致させる
操作名を明示するルールにした理由は、それだけではありません。
ここで定義された URI は、多くの場合、コントローラ層によって処理されるでしょう。
コントローラ層にはメソッドが定義されておりますが、時として、APIのエンドポイントとの結びつけが、直感的でないケースがあります。
しかし、「操作名を末尾に記述する」というルールを設けることによって、「URLの末尾とコントローラ層のメソッドと一致させる」事ができるようになります。
例えば、以下の APIは、以下のメソッドとの結びつけができるようになります。
/item/show
/item/store
class ItemController extends Controller
{
public function show(ShowRequest $request)
{
// your coding here
}
public function store(StoreRequest $request)
{
// your coding here
}
いかがでしょうか。
APIのエンドポイントを見るだけで、メソッド名を判別する事ができる事になり、ソースコードの可読性を上げる事ができます。
それだけでなく、開発者はコントローラ層のメソッド名を付ける時、余計な考えを挟む必要がなくなります。
ただし、URLのアクション名については、一考の余地があると考えております。
今回のプロジェクトでは、バックエンドに Laravel を使用し、PHP のメソッド名はキャメルケースを使用していました。
そのため、バックエンドで使用されている言語のルールと合わせるために上記のルールを適用していましたが、これが適切なのかどうかは、十分に検討する必要があると考えます。
なぜなら、URLの命名ルールは、バックエンドの言語に左右されるべきではないからです。
例えば、バックエンドの言語を Go言語に置き換えると、不自然な部分が出てきます。
なぜなら、Go言語は、メソッド名にアッパーキャメルを使う事が多いからです。
なので、URLの部分は、バックエンドの言語が何であれ、チェインケースを使うという考えもアリでしょう。
余談ですが、URLにおいて複数の単語をつなぎ合わせる場合、かなりバラバラで、業界標準のルールが無い状態です。
私が持っている「Web API The Good Parts」という本では、以下の例が挙げられていました。
サービス | ルール | 例 |
---|---|---|
スネークケース | /statuses/user_timeline | |
YouTube | キャメルケース | /guideCategories |
ドットケース | /me/books.quotes | |
Bit.ly | チェインケース | /v1/people-search |
Disques | キャメルケース | /api/3.0/applications/listUsage.json |
どれも決定的というわけではありませんが、チェインケース(ハイフンで繋ぐ)が良いという意見が多く見られるようです。
その理由は、Google がハイフンを推奨しており、SEOに有利、という説があるのが根拠のようです。
この本によると、Google の検索エンジンは、ハイフンは単語のつなぎと見なすが、アンダーバーで繋がれた場合は1つの続きの単語と見なす、といった意見が見受けられるとのことです。
しかし、2021年5月時点において、Google がそのように公式に発表している資料を、少なくとも私は見つける事ができませんでした。
とはいえ、ハイフンで繋ぐケースは非常に多いので、特別な理由が無ければ、ハイフンで繋ぐようにしてもよいのではないかと思います。
(もちろん、冒頭で紹介したように、バックエンドの言語が変わる可能性が極めて低ければ、バックエンドの言語のルールと合わせるのもOKです)
URL の一部を GETパラメータとして使用しない
例えば、以下のような URL構成があったとします。
一見、何の問題も無いように見えます。
しかし、GETパラメータで渡す値が、複数存在する場合はどうなるでしょうか。
APIを動作させるために、多数のパラメータが必要となるケースは珍しくないと思います。
その場合、パラメータの一部は URL の一部として渡し、パラメータの一部は "?" と "&" で渡す、という構成になるでしょう。
その構成を、私は煩雑ではないかと考えました。
なぜなら、設計者は「どのパラメータを URL で表現し、どのパラメータを "?" と "&
" で渡すか」という事を考えなければならないからです。
また、使う方も、「URLに含まれている内容は、何の値だろう」という情報を知る必要があります。
「URLの一部を GETパラメータとして使用しない」というルールを設ける事により、そういった事に気を回さず、GETパラメータが定義されている場所を共通認識としてチームに共有する事ができます。
終わりに
いかがでしたでしょうか。
いくつかのルールは、受け入れがたいかもしれません。
しかし、現在のプロジェクトにおいて、これらのルールが受け入れられ、プロジェクトが上手く回っているという実績があります。
API 設計には、絶対的な正解はありません。
今回、私が提案したルールが、何かしらの一助になることがあれば幸いです。
Discussion