2016/10/12 社内勉強会で使ったスライドを社外向けに一部加筆訂正したもの
RESTful API の設計のキホン2016/10/12 Hiroki Honda@Cside_
View Slide
なぜこの会を開いたか● 自分の関わった某案件で、API の新エンドポイントのインターフェイスをレビューするときに「こういう風に直して下さい」と指示はしてきたけれど、「なぜそのように直さなければならないのか」までちゃんと説明しきれてなかった○ 納得できてない人もいると思う○ なのでちゃんと説明したい○ そして、誰でも設計できるようになってほしい(重要)
おことわり● API の設計に正解はなく、これから話す指針はあくまで「私個人の考える GoodPractice 」であることをご了承下さい○ マウンティングやめてね … 。○ なるべく、他にどういう流派があるかはあわせて説明するようにします
第一章REST と RPC
Web API のスタイル● REST スタイル● RPC スタイルが主なもの。
REST スタイルの特徴● REST スタイルの API 設計では ROA( Resource Oriented Architecture )という手法が広く知られている● Web API を RESTful にする == ROA にそって設計する● なので ROA の特徴をしっかり理解するのが重要
ROA の 4 つの特徴● アドレス可能性○ リソースがURIを通して表現できること● ステートレス性○ APIリクエストのためのHTTPリクエストがすべて分離・独立していること(前のリクエストに影響されたりしない)● 接続性○ リソースは別のリソースとの関連を表すリンクを持ちうること● 統一インタフェース○ HTTP メソッドを用いてリソースを参照/更新すること○ 後述
リソースとは● URI を持ったデータのこと
リソースの例● 例えば、モバゲーにおける僕のプロフィールというリソースは以下の URI を持っている○ /api/restful/v1/people/32592054/@self
リソースの例● GET すると以下のようなレスポンスが返ってくる(※ 形式は簡略化しています) GET /api/restful/v1/people/32592054/@selfHTTP/1.1 200 OK { "id" : 32592054, "nickname" : "ほんだ", "thumbnailUrl" : "http://sp.mbga.jp/img_u/10000/0.0.gif", "profileUrl" : "http://sp.mbga.jp/_u?u=10000", "hasApp" : true }このオブジェクトがリソース。
ROA の統一インターフェイスHTTP メソッドを用いてリソースを取得/更新する。● GET: リソースの取得● HEAD: リソースの取得。HTTP ヘッダのみを返す● POST: リソースの新規作成● PUT: 既存リソースの置き換え● PATCH: 既存リソースの差分更新● DELETE: 既存リソースの削除
RPC スタイル● XML-RPC, JSON-RPC など● 一言で言うと、制約が少なく、中央集権型
RPC スタイル● RPC エンドポイント、メソッド名、リクエストパラメータ だけあれば利用できるリクエスト例POST /rpc-endpointContent-Type: application/json{ "jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5] }
RPC スタイル● RPC エンドポイント、メソッド名、リクエストパラメータ だけあれば利用できるリクエスト例POST /rpc-endpointContent-Type: application/json{ "jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5] }エンドポイントは 1 個しかない場合が多いリソースの操作方法は HTTP メソッドでなくパラメータで指定基本全部POST
RPC スタイル● エンドポイントが 1 つしかない場合が多い○ リソースごとにユニークな URI を割り当てる REST と比べると対照的
REST か RPC か● REST○ 特徴■ ルール(制約)によって自明なコンセンサスが生まれ、システムを異なる人が分担して設計したとしても、一定の一貫性を担保できる○ 向いているケース■ 複数のエンジニアが API の設計をする場合
REST か RPC か● RPC○ 特徴■ 柔軟性が高く、中央集権型○ 向いているケース■ 限定されたエンジニアが設計をする場合
REST か RPC か● RPCは良く言えば自由、悪く言えばバラバラなものができあがりがち● したがって多くの場合でRESTを選択するべき● ネットを見ると「 REST は考えることが多くてだるいよねー、RPC 最高!」という意見が散見されるが、そう発言する人のほとんどは、REST の「制約によるコンセンサス」というメリットを無視しているので注意
第二章リソース取得/更新の具体例
エントリリソースの取得GET /api/1.0.0/users/20000HTTP/1.1 200 OK{“id”: 100,“nickname: ”nekokak”}単一のリソースのことを一般的にエントリリソースと呼ぶ最後の数字が user id 。URI がリソースを表現する一意のものになっているのがポイント
コレクションリソースの取得GET /api/1.0.0/users{items: [{ “id”: 100, “nickname: ”nekokak”},{ “id”: 100, “nickname: ”zigorou”},],nextCursor: “1000.1476255989”}複数リソースのことを一般的にコレクションリソース と呼ぶ次のページへのポインタ(ページネーション情報)
リソースの新規作成POST /api/1.0.0/usersHTTP/1.1 201 Created{“id”: 100,“nickname: ”zigorou”}(コレクションリソースに対して)新規のエントリリソースを追加、という操作なので、主体は users 。200 OK ではないことに注意作成されたリソースを返すのが一般的
エントリリソースの削除DELETE /api/1.0.0/users/20000HTTP/1.1 204 No Content200 OK で返す人が多いが、レスポンスボディが無いことを明示的にするために、204 No Content がベター※ No Content にするケースが多いというだけで、Content を返してはいけないというわけではない
リソースの置き換えPUT /api/1.0.0/users/100Content-Type: application/json;{“id”: 100,“nickname: ”zigorou”}HTTP/1.1 204 No ConetntPUT は差分更新でなくまるっと置き換える操作であることに注意。差分更新は PATCH 。
リソースの差分更新PATCH /api/1.0.0/users/100Content-Type: application/json;[{ “op”: “replace”, “path”: “/nickname”, “value”: “ねこかく” },{ “op”: “add”, “path”: “/hobby”, “value”: “犬を飼う” }}HTTP/1.1 200 OK{“id”: 100,“nickname”: “ねこかく”,“hobby”: “犬を飼う”}JSON Patch という表現方法。・nickname を「ねこかく」に変える・hobby というフィールドを追加という差分更新をしている。
JSON PATCHPATCH /api/1.0.0/users/100Content-Type: application/json;[{ “op”: “replace”, “path”: “/nickname”, “value”: “ねこかく” },{ “op”: “add”, “path”: “/hobby”, “value”: “犬を飼う” }}⇒ https://tools.ietf.org/html/rfc6902op: operation の略。操作を指定。add, remove, replace, moveなどの語彙が使える。path: 更新するフィールドをJSON Pointer という表現方法で指定。value: 更新後の値を指定。
第三章Web API Bad Practice※ ここから先は、ROA や REST に限らず Web API 全般の話になります。
悪い例: レスポンスがフラットな配列● 複数リソースをフラットな配列で返す○ 後からページネーション情報とか入れたくなったときに詰むので、原則 items: みたいなエンベロープで包む○ ページネーション情報などはレスポンスヘッダに含め、レスポンスボディはリソースだけを返すべきだ、という宗派もある■ 気持ちは分かるけど、ぶっちゃけ使いづらいと思うGET /api/1.0.0/usersHTTP/1.1 200 OK[{ “id”: 100, … },{ “id”: 200, … },]
悪い例: /list みたいな URI● GET /api/1.0.0/users/list みたいな URI○ ROA において URI は「リソースの場所を表現するもの」であることを思い出す○ users が user リソースの集合の意なので、list は蛇足
悪い例: リソースのフィールドをパスに含める● ニックネームの変更でPUT /api/1.0.0/users/nickname?value=nekokak みたいなの○ nickname というリソースがあるなら問題ないが、nickname が user というリソースの単なるフィールドである場合「 URI はリソースを指し示すもの」という原則から外れる○ PUT /api/1.0.0/users ないしはPATCH /api/1.0.0/users で 良い(パラメータで更新フィールドを指定する)
悪い例: 動作を URI に含める● POST /api/1.0.0/notifications/send みたいな URI○ くどいが、URI はリソースの場所を表現するもの。send は行為であってリソースの表現ではない○ send する == notification リソースの新規作成する行為なので、send は不要。POST /api/1.0.0/notifications で良い。
悪い例: 動作を URI に含める (2)● GET /api/1.0.0/search_users みたいな URI○ search は行為であってリソースの表現ではない○ /api/1.0.0/users でクエリパラメータで絞り込み条件を指定できれば OK
悪い例: Limit-Offset のページネーション● ページングが limit-offset 形式になっている○ この方法が悪い理由 (1)■ たとえば最初の 20 件を取得してから次の 20 件を取得するまでの間にデータの追加/削除があった場合、実際に取得したい情報と取得した情報にズレが生じる
悪い例: Limit-Offset のページネーション● ページングが limit-offset 形式になっている○ この方法が悪い理由 (2)■ MySQL などの RDBMS ではlimit 5 offset 10,000 というクエリを発行した場合、「 10,005 を取得して最初の 10,000 を捨てる」という処理が行われる■ つまりページが後ろになるほどスロークエリになっていく
悪い例: Limit-Offset のページネーション● ページングが limit-offset 形式になっている○ 代わりにどうすべきか○ 「先頭から数えて何件目」という Pagenation 情報でなく、「この ID より後のもの」や「この時刻より古いもの」というページネーション情報を提供するのが吉■ id や created_at にインデックスが貼られている限り、クエリは高速○ カーソル方式とか呼ばれたりします
悪い例: Limit-Offset のページネーション● ページングが limit-offset 形式になっている○ どうしても使いたいときは、指定できるページ数を制限すべきでしょう。
● 要求に失敗したならレスポンスは 4XX か 5XX を返すべきPOST /api/1.0.0/usersHTTP/1.1 200 OK{“success”: false}悪い例: 要求に失敗してるのに 2XX を返す
悪い例: パスの不要なネスト● ユーザー ID はアプリを横断してユニークなのに/apps/{app_id}/users/{user_id} みたいな URI○ /users/{user_id} で良い
第四章補足
非同期処理時のレスポンスについて● POST/PUT/DELETE などによるリソースの更新処理を非同期で行う場合は、202 Accepted というステータスコードが用意されているのでそれを用いる○ 201 Created とか 200 OK とか返しちゃ駄目
排他制御処理を行ないたい場合● レスポンスヘッダの Etag や Last-Modified を用いたConditional Request (条件付きリクエスト)という手法を用いるのが一般的。(⇒ RFC7232 )○ いわゆる楽観ロック相当の排他制御をすることが可能○ 話すと長いのでここでの説明は割愛○ 「 Conditional Request 」「条件付きリクエスト」等で各自ぐぐってくだださい。
Web アプリケーションでも ROA をやりたい● 知ってのとおり、Web のフォームは GET, POST しかサポートしてない● オーバーロード POST という手法を用いてHTTP Method をオーバーライドする方法がある● POST /books/{book_id}/delete みたいなのをキモいと感じる人は導入すべきでしょう
Web アプリケーションでも ROA をやりたい● オーバーロード POST の例○ Rails ではフォームの _method パラメータに指定された値に HTTP Methodが上書きされる○ Perl の Catalyst では x-tunneled-method パラメータに指定された値にHTTP Method が上書きされる
参考
● Web API The Good Partshttps://www.amazon.co.jp/dp/4873116864/● Web を支える技術https://www.amazon.co.jp/dp/4774142042/参考文献
END