REST WebAPIのプラクティス
雑なメモ書きの様な内容です。
自分の知識、知恵の引き出しの整理も兼ねてます。
実践的で有用な対応パターンをまとめました。
HTTP Method with URI
| http method | explain | URI |
|GET| ドキュメントリソースの取得| https://api.example.com/v1/items/1 |
|GET| コレクションリソースの取得| https://api.example.com/v1/items |
|POST| リソースの作成 | https://api.example.com/v1/items |
|PUT| リソースの更新 | https://api.example.com/v1/items/1 |
|PATCH| リソースの部分更新 | https://api.example.com/v1/items/1 |
|DELETE| リソースの削除 | https://api.example.com/v1/items/1 |
|HEAD| リソースのメタ情報取得 | - |
Status Code
| status code | explain |
|100 - 199 | 情報レスポンス |
|200 - 299 | 成功レスポンス |
|300 - 399 | リダイレクト |
|400 - 499 | クライアントエラー |
|500 - 599 | サーバーエラー |
RESTの原則
- Stateless
- Addressability
- Connectability
- Uniform Interface
命名規則で意識すること
- シンプル
- 直感的
- 整合性
RESTを選択すること
REST or RPC or Graphql
RESTはHTTPの枠組みに乗っかることで表現力を犠牲にする代わりに、画一化された構造でAPIを作ることができるようにする考え方だという理解。
APIを外部公開しない場合は、表現力の犠牲が目立つので、巷ではREST辛いという話がよく聞かれる。しかし、得られる恩恵は大きい。
RPC方面はこの表現力犠牲が少なく作れるので、台頭してきており。さらに扱いやすさを追求したアプローチとしてGraphqlなどが出てきているという理解。
WebAPIであることを明示的にする
VIEW https://example.com/v1/items
API https://example.com/api/v1/items
API https://api.example.com/v1/items
仮に作るサービスがWebAPIだけだとしても、ぱっと見理解できる方が良いので、apiであることがわかる様に明示的にするのがよい。人に優しくつくろう
バージョン情報を入れる
<Bad>
https://api.example.com/items
<Good>
https://api.example.com/v1/items
https://api.example.com/v1/items?v=1
将来の変更可用性をつくるのと、互換性を保ったまま新規開発を行える様にするため。
パスやクエリパラメータにする含める場合や、リクエストパラメータに含める場合もある。
複数のリソースをまとめて作成/更新/削除する場合のエンドポイントをどう作るか
<Create>
Request POST https://api.example.com/v1/items/bulk_insert
<Update>
Request POST https://api.example.com/v1/items/bulk_update
<Delete>
Request POTS https://api.example.com/v1/items/bulk_delete
複数リソースの操作をbulk insert/update/delete と呼んだりする。
この名称を借りて、URIの接尾辞にbulkをつけ、各HTTPメソッドで呼び出す様にする。
このときリクエストのパラメータは配列で更新データや作成データを付与するイメージ。
途中で処理が失敗して、整合性が保てなることを防ぐために、Transactionでデータの操作を行うのがデフォ。
リソースの一部属性値のみを更新したいだけのエンドポイントをどう作るか
<集合リソース(コレクション)の階層>
https://api.example.com/v1/items
<単一リソース(ドキュメント)の階層>
https://api.example.com/v1/items/1
<属性(アトリビュート)の階層>
https://api.example.com/v1/items/1/name
--------------------------------
<Update>
Request PATCH https://api.example.com/v1/items/1/name
リソースの最小単位を属性と考える。そうした場合に、集合のリソース階層からさらに1階層深い、属性の階層を作り、そのURIで更新処理を行う。
リソースの部分更新は、PUTではなくPATCHを使うのが一般的。
URIのリソース名を複数形で揃えるか、単数系も利用するか
<resource>
https://api.example.com/v1/item
<resources>
https://api.example.com/v1/items
<resource>
https://api.example.com/v1/items/1/owner
結論、どっちでも良い。好みでもある。一般的には複数に揃えたいという人もいれば、単数も使いたいという人もいる。
ただし、リソース自体がコレクションとして存在しているのでは、複数形を使う。
単数を使っても良いのは、システムとしてリソースが1つしかありえない場合か、ネストしたリソースの関連で取れるリソースが単一の場合のみ。
URIの名称には動詞は極力避け、名詞を使う様にする
<Bad>
Request GET https://api.example.com/v1/get_items
<Good>
Request GET https://api.example.com/v1/items
<Hummm...>
Request POST https://api.exmaple.com/v1/items/search
URIの名称に`get`などを含めない様にする。HTTP Method(動詞)を使ってリソース操作を行う思想に則っているため、リソースに動詞が含まれていると論理的な整合性がとれなくなる。
ただし、searchなどは良しとしていたりする。理由は、実装の複雑性をあげるくらいなら、崩した方が楽になるから。
Query Parameterを用いた分岐にするなら、子リソースに分ける
クエリパラメータで頑張りすぎず、サブコレクションの表現を活用する
<Bad>
Request GET https://api.example.com/v1/items?filter=favorited&limit=3&tags=a,b,c
<Good>
Request GET https://api.example.com/v1/items/hot?limit=3&tags=a,v,c
アクションが分かれるので、すっきりする。
アクションが増えても、複雑性を低く保ちながら追加対応できる。
DHH流のルーティングの切り方をする。 カスタムメソッドを使わない
https://api.example.com/v1/items/hot
class Api::V1::Items::HotsController < ApplicationController
def index
# return collection of hot
end
end
Rails固有の話だけど、大事なので。
ControllerでCRUD以外のメソッドを使わない、使いたくなったら新たにControllerを作成する。
カスタムメソッドが用いられるシナリオ
- move
- cancel
- search
- undelete
- activate
画面に密結合なURI、エンドポイントは避ける
<Bad>
https://api.example.com/v1/portal
return item, articles, campaigns, events etc..
<Good>
https://api.example.com/v1/portal/items
https://api.example.com/v1/portal/atricles
https://api.example.com/v1/portal/campaigns
https://api.example.com/v1/portal/events
portal画面で利用するデータをportalというエンドポイント1つで全て賄うよりも、複数のエンドポイントに分けた方が、将来の変更可用性が高くなります。再利用性も上がる
もちろん要求によるので絶対ではない。しかし、昨今のフロンドエンドはRxの普及などにより複数のエンドポイントをうまく使い分けられる様になっているので、分ける方がベター。
深くネストするのを避ける
<Bad>
https://api.example.com/v1/sports/1/players/2/friends
<Good>
https://api.example.com/v1/sports/1
https://api.example.com/v1/players/2/friends
リソースに直接的な従属関係があり、それをURIで表現したい、表現した方がわかりやすくなるなら、ネストを許容する。
しかし、基本的にはネストが深くなると分かりづらくなるため、なるべく階層を低くするのがベター。よくても2階層まで。
ネストは右にいくほど、リソースのサブセットになる様に組む
meといったエイリアスを用いる
<General Use>
https://api.example.com/v1/users/1/articles
<Me>
https://api.example.com/v1/users/me/articles
userのidを求められる箇所で、meというエイリアスを独自のルールで設ける。
meといパラメータが来た時は、認証者自身のリソースを返す様にする。これはuser_idのエイリアス
指定したユーザーのリソースが欲しい場合は、idを渡す
こう定義すると、利用側がわかりやすくなる。ただし、アプリケーションコントローラー(ハンドラー)で判定するロジック(6~10行くらい)を書かなければいけない
アクターを軸に名前空間を分ける
<Customer>
https://api.example.com/v1/customer/items
<Manage>
https://api.example.com/v1/manage/items
APIを利用する人(アクター)に応じて、名前空間を作り。別のエンドポイントとして利用する様にする。
こうすることで、manage側で必要とされる認証処理と、customer側で必要とされる認証処理を分けることができ、アプリケーションコードの構造が明瞭になる。
manageのAPI側が、customer側の挙動を意識しなくてよくなるし、customer側も同様。
ディレクトリ階層が増えて冗長になるという批判もあるが、単純化するための冗長さは積極的に許容されるべきものだと考えている。
actionかviewかを分ける
<リソースの取得>
GET https://api.example.com/view/articles
GET https://api.example.com/view/articles/1
<リソースの操作>
POST https://api.example.com/action/articles
PUT https://api.example.com/action/articles/1
DELETE https://api.example.com/action/articles/1
好みによるが、明示的で分かりやすくなって良い
リソース名の単語をつなげる場合はケバブケースを利用する
http://api.example.com/users/12345/profile-image
スネークケースの場合もあるが、ハイフンが良しとされている。
レスポンスのデータ構造はなるべくフラットにした方が良い
{
'id': 3342124,
'message': 'hi',
'sender_id': 3456,
'sender_name': 'taro yamada',
'receiver_id': 12912,
'receiver_name': 'kenji suzuki'
}
// NGパターン
{
'id': 23245,
'name': 'taro yamada',
'profile': {
'birthday': 3456,
'gender': 'male',
'language' : ['ja', 'en']
}
}
// OKパターン
{
'id' : 23245,
'name' : 'taro yamada',
'birthday' : 3456,
'gender' : 'male',
'language' : ['ja', 'en']
}
階層的にするより、なるべくフラットにした方がよい。Google json style guide参照する。
レビュー時の観点
- URIの構造が期待されている表現になっているか
- HTTPメソッドが正しく選択されているか
- レスポンスコードが正しく選択されているか
- レスポンスデータが期待する表現になっているか
- リクエストデータが期待する表現になっているか
- URIの意味が理解できる表現になっているか
- 認証処理が期待する動きをしているか
- 認可処理が期待する動きをしているか
GET, HEAD, PUT, PATCH, DELETEは冪等にする
冪等なAPIにすると、クライアント側は安全にAPIを利用することができる様になります。ここでの安全というのは操作を行った際の副作用による失敗を考慮する必要性がないという意味です。
例えば、クライアントがネットワーク接続エラーを理由にリクエストに失敗した場合、もう一度リクエストを行えば同一の結果が返ってくるので、失敗したとしても特殊な対応を必要とせず。楽に再実行を行えます。
パスパラメーターとクエリパラメータの使い分け
Request GET https://api.example.com/v1/items?category=1
Request GET https://api.example.com/v1/categories/1/items
教科書的には任意の値はクエリパラメータ、必須の値はパスパラメータといった住み分けができると世間的に言われている。
イメージとしては、パスパラメータは集合の中の一部を取り出す感じ。クエリパラメータは集合全体の中から絞り込むイメージ。
この住み分けには論理的な正しさがあるが、絶対解ではないと思う。
このルールにしばるとネストが深くせざるをえなくなったりするので、厳密に従う必要はない。
ただし、LSKDsの場合は縛ってある方が一貫性があり、使いやすくはなりそう。SSKDsの場合は自分たちが分かりやすいかを重視して決めるのがよい。
直接的な関連リソースの表現はパスパラメータ、間接的な関連リソースの表現はクエリパラメータ
1. Request GET https://api.example.com/v1/items/ranking?user_id=1
2. Request GET https://api.example.com/v1/users/1/hogehoge/3/hugahuga/4/items/ranking
例がいまいちだが、2の様なネストが深くなるURIの表現になった場合は、1のようなクエリパラメータを使って、端的な表現にする。
URIはデータの関係性のインターフェースではない。なので、リソースの関係性とURIの表現は別で考える。
パラメータの値でデータを予測されたくない
Request GET https://api.example.com/v1/users/1
Request GET https://api.example.com/v1/users/1
Request GET https://api.example.com/v1/users/9e3452f322ec04bbe4
SSKDsの場合、特に気にもしないという感じの話が多いが。もし、URIのパスパラメータ or クエリパラメータの値からデータを予測されたくないという要望があった場合、例えば、use idが機械採番の値で1332などユーザー数を予測されるのが嫌だ、とかです。
独自で特定されにくいUUIDをランダム値で生成し、リソースのidとして利用する形で対応する
SNS認証系はOAuth2.0を使う
SNS認証系(Google Login / Facebook Login / Twitter Login)は、OAuth2.0の仕組みに則るのが無難。
ちなみに、OAuth2.0は認可のフレームワークであり、認証にも使えるという立ち位置。認証はOpenID Connect
認証、認可、リソース操作を独立して行う
RESTの話というより、WebAPIとしての話。
認証処理の結果生まれたデータを、リソース操作に活用し、なんなら認可処理ももすっ飛ばしてレスポンスを返すということも、処理によっては可能になる。
例えば、認証ユーザーに紐づくデータを返すエンドポイントの場合、認証情報だけで値が取得になる場合など。
動作的にはこれは何も問題はないのだが、基本的な考え方としては、処理を端折らず、独立した処理にする。たとえそれが冗長になったとしても。
その理由は、分かりづらくなるから。認証に付随してリソース操作をする場面と、そうじゃない場面。認可をする場面と、しない場面、でも認証が通ったらOKにする場面など、有効なケースパターンが複数存在すると混乱してしまう。人はそんなに賢くない。
認証処理、認可処理、リソース操作処理。これらは独立した存在として認識し、それらをコラボレートさせて表現するのがアプリケーション層。
という意識で実装するのがよろしいのかなと。
WebAPI設計に絶対解はないが、ベタープラクティスはある
つい絶対解を見つけたくなるんですけど、そんなものはないです。
けど、ベターになるプラクティスはあるので、それらを取り入れながら、場合によっての最良設計をしていくのが良いと思っている。
自分自身いろいろと考え抜いた中での結論。
ベタープラクティスの引き出しを多く持ち、要求に対して場合の良い意思決定を積み重ねることが、設計センスを磨くことだと思う。
ドキュメントなどでの記し方
POST /v1/items HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer oauth2_token
{
"name" : "train"
"price" : 100000000
}
参考になるREST WebAPI
- Twitter
- Facebook
- LinkdIn
- Youtube
- Qiita
- Garoon
ひとこと
改めてWebはすごいな。
noteの記事は自サイトに移行しています。