Developers.IO 2018 で「API 設計」の話をしてきた #cmdevio2018

599件のシェア(そこそこ話題の記事)

緊張すると声がアムロ・レイになる都元です。 ここからしばらく、キャッチコピーの迷走期が始まりますのでよろしくお付き合いください。

さて、去る 10/5 (金) 秋葉原 UDX にて開催された Developers.IO 2018、その中で 「クラスメソッドにおける Web API エンジニアリリングの基本的な考え方と標準定義」 という仰々しいタイトルで1講座持たせていただきました。

スライド

話したかったことと、話したこと

本セッションで話したかったことはだいぶ多岐にわたり、当然 40 分では話しきれないので、当初は次の 2 テーマに絞ってお話しようと考えてスライドを作っていました。

  • アプリケーション動作ログガイドライン
  • RESTful / リソース指向 API 設計

しかし実際にスライドを作ってみると、それぞれで 40 分の規模となってしまい…。 ログの話は断腸の思いで見送りとさせていただきました。

ただし、スライドはもうあるので、どこかで呼んで頂ければいつでもお話しようと思います。 Developers.IO 2018 福岡とか札幌とか、呼んでくれていいんだよ? チラッチラッ

さて、本セッションでとにかく皆様に持って帰って頂きたかったのは次の 4 点です。

  • APIアクションという設計単位を意識しよう。
  • URL パスや HTTP メソッドはファイルシステムメタファで考えよう。
  • リソースその表現は分けて考えよう。
  • 一覧機能で検索を頑張るのは、ほどほどにしよう。CQRSもどきを導入するのも一つの手。

ここからは、その内容に踏み込んでいきます。

API アクション

Web API を設計していくとき、それが RESTful か RPC かにかかわらず、まずは次のような視点で入出力を考え始めると思います。

  • リクエスト: URL、HTTP メソッド、リクエストヘッダ (あれば)、リクエストボディ (あれば)
  • レスポンス: ステータス、レスポンスヘッダ (あれば)、レスポンスボディ (あれば)

これらの要素は設計や実装に伴って必須なので、自然とさまざまな考慮をすると思います。 しかし我々はこれらに加えて、これ以前に「API アクション」という設計単位を考えることにしています。

名前をつけるということ

API アクションとは謂わば Web API におけるメソッドで、これに固有の名前を付けていきます。 名前を付けるという仕事は、「そのもの」と「それ以外のもの」を区別する効果があります。

少々余談になりますが「これの、どこまでが赤でしょうか?」という問題があります。しかし色にどの粒度で名前をつけるかを示すことによって、その境界線がはっきりしてきます。

  • 赤、紫、青、水色、緑、黄色
  • 赤、赤紫、紫、紺色、青、青緑、緑、黄緑、橙

閑話休題。これは API も同様で、ある複数の API 呼び出しを考えたとき、これが「同じ機能呼び出しのパラメータ違い」なのか「全く別の機能呼び出し」なのか、 API アクション名によって明確に区別できるようになります。

具体的に、次の API 呼び出しがあったとします。これらは「同じ API 呼び出しのパラメータ違い」でしょうか。それとも「それぞれ別の API の呼び出し」でしょうか。

  1. GET /orders : すべての order を一覧する
  2. GET /orders?lang=ja_JP : すべての order を日本語で一覧する
  3. GET /customers/CUSTOMER_001/orders : CUSTOMER_001 の order を一覧する

これに答えるのが API アクション名です。ここで例えば 1, 2 には ListOrders、3 には ListCustomerOrders という名前がついていれば、 3 だけが別の API であることがわかります。

API アクションの分類

API アクションはおおむね次の 8 種類に分類しています。 もちろん、他にもアクションの種類はありえますが、我々はこの 8 種で 9 割以上カバーできています。

action 説明 安全性 冪等性
List 全リソースを(絞り込まずに)一覧する操作。ページングはする。 あり
Create リソースを新規に作成する操作。 なし
Get 単一のリソースを取得する操作。 あり
Patch 既存リソースを部分的に変更する操作。 なし 不要
Upsert リソース全体を置換、既存リソースがなければ作成する操作。 なし
Delete リソースを削除する操作。 なし
Process その他、上記に分類できない複雑な更新や計算などの操作。 なし 不要
Query 全リソースの中から、特定の条件で絞り込んで一覧する操作。 あり

ユースケースと API アクション

API アクションは、ユースケース毎に分けて定義しましょう。

例えばよくある Emp-Dept モデルにおいて、次のような要件があったとします。

  • 従業員リソースには住所氏名などのプロフィール情報の他に、給与や所属部門というプロパティも持っている
  • 従業員情報の更新がしたい。

このとき、一口で「更新」といっても次のような想定している場合...

  • 従業員が、自身のプロフィール情報を更新する
  • 管理者が、従業員の給与や所属部門情報を更新する

これらはいわゆるアクターが異なりますので、別のユースケースになります。 従業員が自分自身の給与情報を自由に設定できたらパラダイスダメですね。

権限は、ある API アクションが実行できるか否か (つまり boolean) を決めるものと位置づけています。 つまり「アクション自体は実行できるが、更新対象のプロパティによって拒否することがある」という設計は NG です。

上記の例で言えば、UpdateEmpProfile アクションは従業員が実行可能、 UpdateEmpSalary や UpdateEmpDept は管理者のみが実行可能、という整理をします。

同様に、次のような API アクション定義も NG です。

  • 正社員は ListEmp で全件一覧できるが、業務委託スタッフは自分の所属部門だけ。
  • 管理者は GetEmp で給与プロパティを見られるが、従業員は見られない。

これらも「権限によって出力結果が異なる」という設計になってしまい、権限がアクションの実行可否以上のことを決めている例です。

これは ListEmp (全従業員一覧) , ListDeptEmp (特定部署下の従業員一覧), GetEmp, GetEmpForPublic ... などのアクションに分割して定義していくことを推奨しています。

ファイルシステムメタファ

さて、我々が RESTful な API を設計していく際、サーバー側が保持する状態はファイルシステムであるかのように振る舞うように整理しています。

古き良き時代、Apache httpd という HTTP サーバーは GET リクエストに対して /var/www/html というファイルシステム内のディレクトリに配置してある 静的な HTML ファイル (や画像ファイルなど) を返す、というのが基本機能でした。POST では特別に CGI によってプログラムを起動する、という程度のものでしたが、 いつの間にやらほぼすべての HTTP リクエストをプログラムによって取り扱い、動的にレスポンスを返す時代が到来していました。

現代の API サーバーにおいてもこの起源 (古い記憶) を忘れず、表向きはあたかもサーバー内に静的なファイルがあって、GET によってそれを取得している、という操作感を提供します。 これによって、利用者にとっては驚きの少ない、直感的な API 群が整理できると思っています。

method 指定したパスに対する動きのメタファ
GET cat または ls コマンドの実行
PUT リクエストボディの内容を、リダイレクトで書き込む (作成または上書き)
PATCH 既にあるファイルに、リクエストボディの内容 (修正) を適用して inplace 書き込み
DELETE rm コマンドの実行
POST 何らかの実行ファイルの起動

例えば、次のようなファイルシステム構成があったとします。

ちなみに Emp については /emps 内と /depts/*/emps 内に同じファイルがありますが、これは $ ln -s /emps/ezaki /depts/dev/emps/ezaki のように シンボリックリンクを張ったものと認識してください。実体はあくまでも /emps 直下のもので、これがメインです。

.
|-- depts
|   |-- dev
|   |   `-- emps
|   |       |-- ezaki
|   |       `-- kanou
|   |-- admin
|   |   `-- emps
|   |       `-- ushijima
|   `-- sales
|       `-- emps
`-- emps
    |-- ezaki
    |-- kanou
    `-- ushijima

ここで GET /depts/admin/emps というリクエストは次の linux コマンドと同義と考えます。(出力は JSON になっちゃってますが、意味合い的に)

$ ls /depts/admin/emps
[
  "ushijima"
]

また GET /emps/ezaki は次のとおりです。

$ cat /emps/ezaki
{
  "emp_code": "ezaki",
  "name": "柄崎貴明",
  ...
} 

そして PUT /emps/kanou 具体的に

PUT /emps/kanou

{
  "emp_code": "kanou",
  "name": "加納晃司",
  ...
}

は次のようなコマンドと同義です。

$ echo '{
  "emp_code": "kanou",
  "name": "加納晃司",
  ...
}' >/emps/kanou

リソースとリソース表現

リソース指向とは

リソース指向というのはなかなか説明が難しい概念です。これを捉えるためには、「オブジェクト指向」と対比するのが良いのではないかと思っています。

オブジェクト指向が

  1. オブジェクトを参照する変数があり、
  2. このオブジェクトに対してメソッド呼び出しをして、メッセージパッシング
  3. そのメッセージ内容には、引数としてパラメータを渡せる
orders.create({
    "id": 10
});

だとしたら、リソース指向は

  1. リソースを参照する URL があり、
  2. このリソースに対して HTTP メソッドによって、リクエストを実行
  3. そのリクエストボディには、パラメータを渡せる
POST /orders

{
    "id": 10
}

となります。このような考え方をベースに設計した API をリソース指向 API と呼びます。

プライバシー保護の観点などから、従業員の表現には複数ある

先程「管理者は GetEmp で給与プロパティを見られるが、従業員は見られない」という要件例を示しました。 つまりレスポンスには次の 2 つがありえます。

{
  "emp_code": "ushijima",
  "name": "丑嶋馨",
  "tel": "00-1111-2222",
  "salary": 12345678,
  "dept_code": "admin"
}
{
  "emp_code": "ushijima",
  "name": "丑嶋馨",
  "tel": "00-1111-2222",
  "dept_code": "admin"
}

そこで、ここでは「/emps/ushijima という概念は唯一ですが、その表現には 2 通り (場合によってはそれ以上) の表現がある」という理解をします。

リソース (/emps/ushijima) は URL によってポイント可能で API アクションの対象となる概念そのものを表しています。 一方で、上に示したような JSON は、正にリソースを直列化形態として表現したものです。

ちなみに RESTful の REST は REpresentation (表現) State Transfer の略です。

HTTP メソッド足りない問題

GET /emps/ushijima の結果 (レスポンス) を権限に応じて出し分けるような設計は、先に言った通り NG です。 またこれまでの指針の通り、ユースケース毎に異なった API アクションを設計していくと、対応する HTTP メソッドが足りなくなってきます。

この辺りは意見の分かれるところかと思いますが、我々は URL パスの末尾に _ アンスコから始まるパートを付与し、 これを HTTP メソッドに対する修飾子として解釈するようにパス設計を行うことにしました。

具体的に、管理者用の従業員参照は GET /emps/{emp_code} とし、一般従業員用の従業員参照は GET /emps/{emp_code}/_for-public などとします。 後者は GET_for-public という HTTP メソッドを /emp/{emp_code} というリソースパスに対して実行しているイメージであると解釈します。

同様に POST /emps/{emp_code}/_update-salaryPOST_update-salary という HTTP メソッドのように考えます。

CQRS もどき

一般的に、コマンド (データの更新) とクエリ (データの要求) は 1 つのインターフェースを介して実行します。

CQRS は、データに対するアクセスを「コマンド (命令)」と「クエリ (問い合わせ)」に分けて、それぞれを受け付けるインターフェイスを分けて整理しましょう、という考え方です。 では、なぜコマンドとクエリを分けたいのかと言いますと。コマンドもクエリも、それぞれ仕様は複雑なものですが、複雑さの方向性が異なるからです。

コマンドは、書き込みを行うがゆえにトランザクションを必要とする場合があります。また、クエリよりも強く複雑な権限評価が必要になりがちです。

一方でクエリは、検索条件が多岐に渡り、ソート順やページングの扱いも複雑です。また、コマンドよりもスケーラビリティ要求が高い傾向もあります。 また、出力の形式もバラエティに富んだものになりがちです。一言で言うと、ユースケースを限定しづらいのです。

そこで、List API アクションに検索の機能を持たせるのではなく、List は単純に全件を総ナメする使い方に限定します。 そして検索機能は別途 Query という API アクションに整理する、ということをしています。

ただし、本来の CQRS は上図の通り、「読み込み」と「書き込み」で責務分離をしていますが、我々の Web API においては「PKを使ったシンプルな参照 Get」と「全件の一覧 List」については 「クエリ」ではなく「コマンド」側に分類しています。(なのでもどきという認識です。)

この辺りは以前AWSクラウドデータストレージ総論でもお話したので、併せて参考にしてください。

まとめ

以上の考え方から、ざっと Emp-Dept モデルの API を列挙してみると、次のようになります。

API アクション HTTP リクエスト 説明
ListDept GET /depts 部署一覧
CreateDept POST /depts 部署作成
GetDept GET /depts/{dept_code} 部署取得
UpdateDept POST /depts/{dept_code} 部署更新
PatchDept PATCH /depts/{dept_code} 部署部分更新
UpsertDept PUT /depts/{dept_code} 部署置換
DeleteDept DELETE /depts/{dept_code} 部署削除
ListEmp GET /emps 従業員一覧
CreateEmp POST /emps 従業員作成
GetEmp GET /emps/{emp_code} 従業員取得
UpdateEmp POST /emps/{emp_code} 従業員更新
PatchEmp PATCH /emps/{emp_code} 従業員部分更新
UpsertEmp PUT /emps/{emp_code} 従業員置換
DeleteEmp DELETE /emps/{emp_code} 従業員削除
UpdateEmpSalary POST /emps/{emp_code}/_update-salary 従業員給与更新 (for 管理者)
UpdateEmpProfile POST /emps/{emp_code}/_update-profile 従業員プロフ更新 (for 本人)
ListEmpForPublic GET /emps/_for-public 従業員一覧 (給与見えない版)
GetEmpForPublic GET /emps/{emp_code}/_for-public 従業員取得 (給与見えない版)
QueryDept POST /query/depts 部署検索
QueryEmp POST /query/emps 従業員検索

※ もちろん、必要に応じてこれ以外の API アクションも定義しますし、不要なものは除外することもあると思います。

この設計は、ある API リクエストの HTTP メソッドとパスが決まれば、API アクションが決まるようになっています。 そして、「API アクションが決まれば、すべてが決まる」という状況です。

API アクションが決まれば、次に挙げる事項が芋づる式にすべて確定するのです。

  • 処理の内容が決まる。(権限などのコンテキストに応じて処理内容は変わらない。)
  • レスポンスとして帰ってくるリソース表現が決まる。(権限などのコンテキストに応じてレスポンスは変わらない。)
    • ということは、レスポンスとして帰ってくるリソースも決まる。
  • 必要な権限が静的に決まる。つまり権限定義が決まる。
  • 使うべき HTTP メソッドが決まる。
  • HTTP リクエストを送るべき URL が決まる。
    • ということは、操作の軸 (対象) となるべきリソースも決まる。

ここまで整理できると、開発や運用の現場で普段から「API アクション名」を使って会話ができるようになります。 そしてこの名前はテストの命名にも有効活用できますし、ドキュメントの中でも積極的に使っていくべき言葉になっていきます。