REST API 設計書
Overview
REST APIでの設計についてまとめているセクション。
基本的には「Web API The Good Parts」を参考にしている。
いい設計の考え方
- ユースケースを考えるべき。そうすると目的がはっきりする。
- APIはデータベースのインターフェースではない。そのためのユースケースになる。
データベースのテーブルに紐付ける1対1のAPIは危険
データベースのテーブルを直接操作するようなAPIを作ることもできる。が、SQLを文をただ包んだだけの設計では使いやすいAPIにならない。
なぜならそんなAPIでは、データが内部的どのように格納されているか、どういうリレーションを持っているかなどを理解していないと使うことができないため。
また内部構造を公開してしまうのはセキュリティを考えても危険なこと。
エンドポイントの設計とリクエストの形式
API パス設計
ホスト名 api.example.com の部分については大文字・小文字はもともと無視される仕様。
そのためパス名も小文字にする方が統一できる。
HTTPにおいてURIは「スキーマとホスト名を除いては大文字と区別される」と使用に書いてある。
したがって、エンドポイントを小文字としている以上大文字を入れたらエラーにするべき。
HTTPメソッドとエンドポイント
1つのURIエンドポイントに異なるメソッドでアクセスしリソースをどう扱うかをきちんと分離して扱うことができる。
GET
リソースの取得を表す。
POST
指定したURIに属する新しいリソースを送信する。
https://api.example.com/v1/friends
送信したデータは指定したURIに従属したものとなる。
従属とは下位に属する意味。
例
ディレクトリの中にファイルが入っている場合、ファイルがディレクトリに属している関係。
したがってディレクトリやカテゴリなどデータの集合を表すURIに対してPOSTを行うと、新しいデータがその配下に作られるイメージ。
PUT
更新したいリソースのURIそのものを指定し、その内容をすべて書き換える。
PUTメソッドを使用する場合は、更新内容はリクエストボディに含めることができる。
PUTには冪等性がある。
https://api.example.com/v1/friends/12345
PUTは送信するデータで元々のリソースを完全に上書きするというものになる。
一部だけ書き換えたい場合はPATCHメソッドを使用する。
PUTの冪等性(べきとうせい)とは、同じリクエストを何度繰り返しても、結果が変わらない性質のことです。具体的には、PUTメソッドを使ってリソースを作成または更新する際に、同じリクエストを複数回送ってもサーバー上のリソースの状態は変わらず、同じ状態に保たれることを指す。
例えば、PUTで特定のユーザーのプロフィール情報(名前やメールアドレス)を送信した場合は以下の通り。
- 1回目のリクエストでサーバー上のリソースが更新される。
- 2回目以降のリクエストを送っても、同じ内容であれば、サーバー上の状態はすでに更新されているため、何も変わらない。
これは、POSTとは異なり、同じリクエストを複数回送信しても状態に影響がないため、安心してリトライ可能であり、エラーが発生した場合でも再度リクエストしやすいというメリットがある。
PUTが冪等である理由は、リクエストがリソース全体を「置き換える」動作をするため
DELETE
PATCH
更新したいリソースのURIそのものを指定し、その内容を一部書き換える。
https://api.example.com/v1/friends/12345
PATCHメソッドには冪等性がないため注意。
POST以外のメソッドをPOSTを使って表現する。
HTML5の Form ではGETとPOSTのみがサポートされている。
この制限はHTMLの歴史的な理由によるもので、他のHTTPメソッド(PUT や DELETE など)は form 要素では直接サポートされていない。
メタ情報として本当はこのメソッドを使いたいということをサーバーに送信する。
X-HTTP-Method-Overrideのリクエストヘッダーを用いるX-HTTP-Method-Override: DELETE_methodというフォームパラメーターを利用する方法user=testuser&_method=PUTRuby on Railsなどが採用している。
APIのエンドポイント設計
- 覚えやすくどんな機能を持つURIなのかが一目で分かる。
- 短く入力しやすいURI
- 人間が理解できるURI
- 改造しやすいURI
- サーバー側のアーキテクチャが反映されていないURI
- ルールが統一されたURI
https://api.example.com/v1/users # GET: ユーザー一覧の取得
https://api.example.com/v1/users # POST: ユーザーの新規登録
https://api.example.com/v1/users/:id # GET: 特定のユーザー情報の取得
https://api.example.com/v1/users/:id # PUT/PATCH: 特定のユーザー情報の更新
https://api.example.com/v1/users/:id # DELETE: 特定のユーザー情報の削除
一覧を意味するために list とつけることがあるが、URIを見れば分かるため不要。
自分の情報のエイリアス。
自身の情報を取得する場合には /users/:id となるが、自分の情報を知るためにIDを用いるのは不便になる場合がある。
そこで利用されているのが me または self というキーワード。
APIを分けることで、他人の個人情報を丸見えになってしまうバグの混入を防ぐことができる。
/users/me
エンドポイントがなぜ名詞なのか
HTTPのURIがそもそもリソースを表すものであるため。
HTTPメソッドが動詞を表す耐え、その組み合わせを使うことが最もシンプルに行いたいことを表すことができるため。
そのため動詞は極力エンドポイントには入れない。
エンドポイントはハイフンか_(アンダースコア)か
ハイフンで統一すべき。
URIのホストはハイフンを許可されているため(_は許可されていない)
そのため統一した方がいい。
検索API
相対位置指定
limit と offset は一般的だが問題がある。
ズレが生じる問題
更新頻度の高いデータだと先頭から件数を指定して次の行数を取るときに、新しいデータが追加された場合にズレが起きる。
相対位置(offset)を利用する問題点
相対位置(offset)を利用する問題点はパフォーマンスにある。
APIのバックエンドではデータベースで相対的な数値を使ってデータを取得すると非常に速度が遅くなるケースもある。
MySQL は、先頭から何件目かを調べるために先頭から数を数える処理が行われるため。
そのためレコード数が大きくなれば大きくなるほど遅くなる。
絶対位置
先頭から数えて何件目ということよりも、指定した日時よりも前、といった方法で指定する。 取得した最後のデータのIDや時刻を記録しておいて、このIDよりも前のもの、この時刻よりも古いものと指定する方法。
レスポンスデータの設計
データ内部構造の考え方。
APIのアクセス回数がなるべく減るようにすること。
APIのアクセス回数が増えると利用者にとって煩雑になる。
またHTTPのオーバーヘッドも上がってアプリケーションの速度が低下する。
Web APIは単なるデータベースのアクセスインターフェースではなく、アプリケーションのインターフェース。
そのためそのアプリケーションの特性を踏まえた上で、利用者が使いやすい構造にする。
またWebサイトのAPIなど、多様性があまりない場合はよりユースケースを想定したAPI設計ができる。
APIが単なるデータベースのそれぞれのテーブルの内容を返すだけのものだった場合は、設計を見直した方が良い。
- データフォーマットを決める(基本はJSON)
- レスポンスの内部構造を決める
POSTやPUT, DELETE
- 更新したデータを返す。
更新したデータをreturnするという方法。こんな風にデータが直りましたと示す。 - HTTP STATUSで
201や204を返してあげる方法。
201は情報が作成された意味
204は通信は成功してるけど戻り値がないという意味。
データフォーマット
JSONをデフォルトにし、需要や必要があればXMLに対応するのが良い。
レスポンスの内容をユーザーが選べるようにする
APIでできるだけ返すが、クライアントが必要以上に大量のデータを受け取らなくてはいけなくなるのも事実。
そこでよく取られる手法は取得する項目を利用者が選択可能にすること。
https://api.example.com/v1/users/12345?fileds=name,age
フィールドを省略した場合には、レスポンスのセットなどを定義しておきそれを返すなど柔軟にできる。
エンベローブは必要か
実際のデータはレスポンスに入れ、APIに共通のメタデータを入れること。
{
"headers": {
"status": 0,
},
"response: {
// 実際のデータ
},
}
エンベローブは冗長になるため不要。Web APIは基本的にHTTPを利用しているためHTTPのヘッダーに入れれば良い。
データはフラットにすべきか
GoogleのJSON Style Guideでも「なるべくフラット、階層構造を持った方がわかりやすい場合もある」と記載があるので場合による。
レスポンス key
- 慣例のkey nameにする
- なるべく少ない単語数で表現する
- 複数の単語を連結する場合、その連結方法はAPIを全体を通して統一する。
- 省略は基本使用しない
JSONがベースにしているのはJavaScript。そのためキャメルケースが良いとされている。
スネークケースのが読みやすい研究結果もあるぐらい。
そのためAPIでどちらかに統一するのが正。
性別のデータ
- sex
- 生物学的な性別
- gender
- 社会的・文化的性別
生物学的な性別が必要な場合は sex を使い、そうでなければ gender を使う。
日付のフォーマット
利用するユーザーが不透明の場合は RFC 3339 を使うのが一番良い。
またUnixタイムスタンプも良い。
理由としてはこのフォーマットは数あるデータフォーマットの問題を解決したため。
またタイムゾーンについてはAPIを日本で提供している場合は +09:00 を使ってもいいが、インターネットが世界と繋がっているため +00:00 を使うのがわかりやすくおすすめ。
2024-11-21T13:00:12+00:00
2024-11-21T13:00:12Z
大きな整数とJSON
JavaScriptでは数値をすべてIEEE 754標準の64ビット浮動小数として扱うため大きな数値を扱うと誤差が出る。
解決方法としては大きな数値とともに、文字列で返してあげるのが良い。
{
"id": 22222222222,
"id_str": "222222222",
}
レスポンスのトップレベルは配列かオブジェクトか
以下の理由からオブジェクトのがよい。
- レスポンスデータが何を示しているものかがわかりやすくなる。
- レスポンスデータをオブジェクトに統一できる。
- セキュリティ上のリスクを避けることができる。
トップレベルが配列であるJSONはJSONインジェクションという脆弱性に対するリスクが大きくなる。
トップレベルがオブジェクトだとJavaScriptで読み込んだときにブロックとして判定されるためリスクを避けられる。
しかし配列だと正しいJavaScriptとして認識され問題なくブラウザに読み込まれる。
トータルカウントは返すべきか
全体の件数を計算する処理は重くなりがちなので、件数を返すべきかは考慮すべき。
必要でない場合はむやみに返すべきではない。
メンテナンスモードのレスポンス
ステータスコード 503 で返す。
終了時間をクライアントで表示する。
また、Retry-After というHTTPヘッダーを使っていつメンテナンスが終わるかを示す。
このヘッダーは「次いつアクセスしてください」と言う意味を表すために HTTP 1.1 で策定された。
SEO的な観点であればGoogleも推奨している。
クライアント側でも 503 はサービス停止と認識し、Retry-After があった場合には指定された時間まで待ってから再度アクセスする実装が期待される。
HTTPの仕様を最大限利用する
公開するAPIの仕様や挙動を決定する際の原則の1つとして既存の標準仕様を遵守すること。
Web APIはHTTP上で通信するためHTTPの仕様をしっかりと理解する。
ステータスコードを正しく使う
適切でないステータスコードを返すことはクライアントが適切な分岐を行えない結果を招き、余計な問題を引き起こす危険性がある。
200番台
202 のAcceptedはリクエストした処理が非同期で行われ処理は受け付けたけれど完了していない場合に利用されるもの。
BoxのAPIではファイルのダウンロードをする際に、そのファイルがまだ準備できていない場合に 202 が処理を完了するまでの所要時間を秒で表す Retry-After ヘッダとともに返される
204 は No Content
プログラム言語でリストなどのデータを削除する際に削除されたデータが戻り値として返ることも多い。
そのためPUTやPATCHなどは更新されたデータを返す。
DELETEは返さない。
キャッシュとHTTP仕様
HTTPのキャッシュには2つのタイプがある。
HTTPのキャッシュはRFC 7234で策定されている。
HTTPではキャッシュが利用可能な状態を fresh そうでないものを stale と呼ぶ。
HTTP1.1の仕様で1年以上未来の日付を送るべきではないとなっている。
以下はキャッシュを利用するメリット。
- サーバーへの通信を減らすため、ユーザーの体感速度を上げることができる
- ネットワーク接続が切れた状態でもある程度サービスを継続できる
- サーバーへの通信回数、転送量を減らすことでユーザーの通信コストを下げることができる
- サーバーへのアクセス回数が減ることで、サーバーの維持費用を抑えることができる
Expiration Model(期限切れモデル)
レスポンスを受け取った時の情報をもとに設定する。
期限が切れるまではネットワークアクセスが発生しなくなる。
期限切れモデルはあらかじめレスポンスデータに保存期間を定めておき、期限が切れたら再度アクセスをして取得するもの。
いつ期限が切れるかをサーバからのレスポンスに含めて返すことで実現ができる。
HTTP1.1では2種類用意されている。
- Cache-Controlレスポンスヘッダ
- Expiresレスポンスヘッダ
Expires HTTP1.0から存在するヘッダで期限切れを絶対時間で(RFC 1123)定義された形式。
Cache-control 現在時刻からの秒数で表す。
どちらかを使用するには返すデータによって変わる。
特定の日時に更新されることがわかっているデータ、天気情報が毎日同じ時間に更新される場合などは Expires でその日時を指定できる。
Cache-controlは更新頻度がある程度限られているものや、更新頻度は低くないものの、あまり頻繁にアクセスして欲しくないものに使える。
Validation Model(検証モデル)
検証モデルは今保持しているキャッシュが最新であるかを問い合わせてデータが更新されていた場合にのみ取得するもの。
今持っているキャッシュが有効かどうかサーバに問い合わせるもの。
そのため都度ネットワークアクセスが発生し、キャッシュが古い場合は新しいデータが返される。
サーバー側 レスポンスに以下のヘッダを加える。
Last-Modified: xxx # 最終更新日付
ETag: "xxxx" # フィンガープリント
更新がなければ 304 変更があれば 200 と更新されたレスポンスを返す。
最終更新日付はそのリソース自体の最終更新日付、複数リソースの場合はその中で最後に更新されたリソースの最終更新日付を使う。
クライアント側
最終更新日付を使って条件付きリクエストを行うには If-Modified-Since ヘッダ。
エンティティタグを使う場合は If-None-Match ヘッダを使う。
Heuristic Expiration(発見的期限切れ)
HTTP1.1では、サーバーの更新頻度や状況を参考にクライアントがキャッシュの期限を自分で決める。
例として Last-Modified を見て、これまでのアクセスの結果から推測してキャッシュしようなどのこと。
そのため基本的にはキャッシュヘッダをAPIが返す。
最低でも Last-Modified などの更新情報はきちんと発信するべき。
キャッシュをさせたくない場合
Cache-Control: no-cache を用いる。
なお Cache-Control: no-cache は厳密にはキャッシュをしないという指定ではなく、最低限「検証モデルを用いて必ず検証する」必要があることを意味する。
機密情報を含むデータで、中継するプロキシサーバに保存をして欲しくない場合には no-store を返す。
Expires を過去の日付や不正な値にした際の挙動はブラウザによって変わるため Cache-Control を用いた方が良い。
Varyでキャッシュの単位を指定する
キャッシュを行う際に、URI以外にどのリクエストヘッダ項目でデータを一意に特定するために利用するかを指定する。 URIが同一でもリクエストヘッダの内容によってデータの内容が異なるケース。
HTTPでは Accept では始まる一連のリクエストヘッダの値によってレスポンスの内容を変更する仕組みが存在する。
これを Server Driven Content Negotiation(サーバ駆動型コンテントネゴシエーション)と呼ぶ。
例として Accept-Language というクライアント側が受け入れ可能な自然言語を指定するヘッダにAPIが対応して、レスポンスデータを変更するなどがある。
そのため同じURIでも Accept-Language の値によって内容が同一ではなくなるため、URIだけを見てキャッシュすると本来取るべきデータを取ることができなくなる。
Varyは特にHTTPのやり取りがプロキシをケイスしており、そのプロキシがキャッシュの機能を有している。
場合に用いられるが、アクセスがプロキシを経由しているかはサーバー側でわからない場合もあるため、サーバー駆動型コンテントネゴシエーションを利用する場合には必ず Vary ヘッダをつける。
堅牢なWeb APIを作るには
ブラウザからのアクススがある場合、XSSやXSRFなどへの配慮をしないといけない。
非同期メソッドを実装する
POST、PUT、PATCH、または DELETE メソッドでは、完了までに時間がかかる処理が必要な場合があります。 クライアントに応答を送信する前に完了を待つと、許容できない待機時間が発生する可能性があります。
その場合は、メソッドを非同期にすることを検討してください。
非同期メソッドは HTTP 状態コード202(Accepted)を返して、要求が処理のために受け入れられたが完了していないことを示す必要がある。
Microsoftが提唱している「非同期メソッド設計」の正しい流れ
各ステップについて
- POSTするとすぐ202で返す(Location付き)
- Locationに向けてフロントがポーリング開始
- サーバーは途中ならstatus=In progressなど返す
- 完了したら303でリソースURLを教える
これがMicrosoft流(=実質世界標準に近い)「長時間実行する非同期メソッド設計」
注意すべきリアルなポイント
- ポーリング間隔は適度にする(数秒~数十秒)短すぎるとサーバ負荷が高まる。
- ポーリング最大回数は決める(タイムアウトする設計)
- 失敗時のエラーハンドリング(status: "Failed")もちゃんとする。
- キャンセルできるならキャンセルAPIも用意(link.rel = cancel)
- フロントが Locationヘッダー を読める設計にしておく(CORS設定注意)
応用編
- Railsでこの「非同期メソッド+ステータスエンドポイント」実装する方法(ActiveJobやSidekiq使う例)
- axiosでポーリング自動化する超シンプルなユーティリティコード
- 「SSE(サーバーからPush通知)」や「WebSocket」でさらにPollingしない世界へ進む方法
1. クライアントがリクエストを送信(例:POST /orders)
- バックエンドは即座にHTTP 202 Accepted を返す。
- このとき、Locationヘッダーに「ステータス確認用エンドポイント」をつける。
HTTP/1.1 202 Accepted
Location: /api/status/12345
2. クライアント側(axiosなど)は 202 を受け取ったら
- Locationヘッダーで渡されたURL(例:/api/status/12345)を
- 一定間隔でポーリングする。
ここは「axiosが自動で待機する」とまではいかないので、自分たちで「ポーリングロジック」を書くのが一般的。
3. 状態確認エンドポイント(/api/status/12345)にアクセスすると
サーバーは今の状態を返してくれる。
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "In progress",
"link": { "rel": "cancel", "method": "DELETE", "href": "/api/status/12345" }
}
statusは例えばこう変わっていく
- "In progress"
- "Succeeded"
- "Failed"
4. もし処理が完了して「リソース」が新しくできた場合
- 状態エンドポイントが HTTP 303 See Other を返す。
- Locationヘッダーに完成したリソースのURLが入る。
HTTP/1.1 303 See Other
Location: /api/orders/12345
これでクライアントは、/api/orders/12345 をGETすれば「最終成果物」を取得できる
Resource
latest vs get methodでqueryを用いる
PUT vs PATCH
MicroSoft:RESTful Web API の設計