はじめに
Web API: The Good Parts
上記の本をやっと読むことができました。大変参考になり、これは必ず身につけなければならないと思い、自分が設計するならば、という立場での設計指針や考慮事項をまとめます。
また、一部古い内容もあり、調べながら執筆時点のベストプラクティスを可能な限り記載しました。
自分のメモ用にもなりますが、他の人にも見ていただき参考になれば幸いです。
ただし、内容は私個人でまとめたり調べてものになりますので、間違っている記載や良くない表現もあると考えております。
ご意見やご指摘があれば是非コメントお願いします。
対象
- Web開発をされている方(主にバックエンド担当者だが、フロントエンド担当者も認識するべき)
- Web APIを利用した事業に関わるプロジェクトリーダー・テックリード
API基本設計
エンドポイントの基本設計
短くて入力しやすいURI
http://api.example.com/service/api/search
http://api.example.com/search
人間が読んで理解できるURI
http://api.example.com/sv/u
http://api.example.com/service/users
大文字小文字が混雑していないURI
http://api.example.com/Users/12345
http://example.com/API/GetUserName
http://api.example.com/users/12345
http://example.com/api/users
複数形にすべきかどうかなどは後ほどのセクションで記載
改造しやすい(Hackable)URI
IDが1-300000の場合alphaエンドポイント、300001-500000の場合betaエンドポイントへアクセスするという場合
http://api.example.com/v1/items/alpha/:id
http://api.example.com/v1/items/beta/:id
http://api.example.com/v1/items/:id
サーバ側のアーキテクチャが反映されていないURI
http://api.example.com/cgi-bin/get_user.php?user=100
http://api.example.com/users?user=100
ルールが統一されたURI
(友達の情報取得)http://api.example.com/friends?id=100
(メッセージの投稿)http://api.example.com/friends/100/message
(友達の情報取得)http://api.example.com/friends/100
(メッセージの投稿)http://api.example.com/friends/100/messages
メソッドの使い分け
メソッド名 | 説明 |
---|---|
GET | リソースの取得 |
POST | リソースの新規登録 |
PUT | 既存リソースの更新 |
DELETE | リソースの削除 |
PATCH | リソースの一部変更 |
HEAD | リソースのメタ情報の取得 |
※GETメソッドでは絶対にサーバ側の情報を変更させないようにする。
クローラからのアクセスがあり、意図しない変更が入ったり、同一生成元ポリシーによるセキュリティ向上の恩恵が受けられない可能性があるため。
エンドポイントの設計の注意点
複数形の名詞を利用する
users、friends、updatesといった「リソースの集合」を表す複数形を利用する。
(DBでもテーブル名が複数形を用いることが適切であるため、この内容に沿うようにする)
名詞である理由は、HTTPのメソッドが動詞を表すためであり、短くて入力しやすいURIの原則に沿うようにするために、動詞は極力エンドポイントに入れないことが基本である。
利用する単語を気をつける
例えば「何かを探す」という場合、searchとfindが候補に上がるが、前者は探す場所を目的語をとり、後者は探すものを目的語に取る。seaechの方が広範囲から特定の条件に合致する複数のものを探すニュアンスがあり、その他のAPIでもsearchがよく使われている。他の例では写真ではpictureではなく、photoの方が使われる。どの単語を使うかは他のAPIを参考にする。
スペースやエンコードを必要とする文字を使わない
URIに利用できない文字はパーセントエンコーディングと呼ばれる表記方法が使われてしまい、可読性が下がってしまうため利用してはいけない。
http://api.example.com/v1/%E3%83%A6%E3%83%BC%E3%82%B6/123
http://api.example.com/v1/users/123
単語を繋げる必要がある場合はハイフンを利用する
公開されているAPIを見る限り、かなりバラバラな状態であるが、ホスト名のルールに沿ってスパイナルケース(ケバブケース)を利用して表現すべきである。
http://api.example.com/v1/users/12345/profile_image
http://api.example.com/v1/users/12345/profileImage
http://api.example.com/v1/users/12345/profile-image
(補足にはなるが、ホスト名はRFC952、1123で規定されており、アンダースコアは許可されていない。)
検索とクエリパラメータの設計
ページネーションの仕組みを検討する
一覧を取得するようなAPIに対して、データが1万以上あり、全てを一度に取得する場合、データのサイズが大きすぎて問題になる。
そこでページネーションと呼ばれる仕組み、SQLではlimitとoffsetのような値を指定して取得できるように実装する。
per_page=50&page=3
limit=50&offset=100
pageやoffsetといった相対的な取得位置でデータを取得すると以下の問題点がある。
- パフォーマンス
- データベース上では相対的な数値を使った位置の指定は遅くなるケースがあるため
- データの不整合
- 更新頻度が高いデータの場合、最初の取得から次の取得の間でデータが更新されてしまい、実際に取得したい位置と取得された位置がズレてしまうことがある
そのため絶対位置指定でのデータ取得を検討する。絶対位置指定とは、最後に取得したデータのIDや時刻を記録してその値よりも前のもの、古いものを指定することである。
この方法を用いることで、上記の問題を解決できる。
絞り込みのためのクエリパラメータとパスの使い分け
以下二つの観点でクエリパラメータに含めるか、パスに入れるかを判断する
- 一意のリソースを表すのに必要な情報かどうか
- 省略可能かどうか
1に関して、一意に特定できる情報であれば、参照したい情報が明確なのでパスに入れた方が良い。
2に関して、省略可能であれば、デフォルト値が勝手に適応されるなどのユースケースが多いので、クエリパラメータを利用した方が良い。
http://api.example.com/v1/users/12345
http://api.example.com/v1/users?since_id=2025
認証・認可について
当然のことではあるが、APIはトークンなどを用いて、認証、認可できる形で提供する。
ここではOAuth2.0などの記載は省略する。
レスポンスデータの設計
データフォーマットとその指定
データフォーマットは基本的にJSONに対応していれば良い。ドメインや外部連携サービスによって、XMLのデータフォーマットのサポートを検討する。
もし複数フォーマットに対応する場合、リクエスト側でデータフォーマットを指定する方法は以下の3種類が考えられる。
- クエリパラメータを使う方法
http://api.example.com/v1/users?format=xml
- 拡張子を使う方法
http://api.example.com/v1/users.json
- リクエストヘッダでメディアタイプを指定する方法
GET /v1/users
Host: api.example.com
Accept: application/json
3が最も行儀の良い方法ではあるが、1の方法でサポートされているものが多い。
基本的には、リクエストヘッダーでの対応を検討する。
内部構造の設計
-
APIのアクセス回数をなるべく減らすようにする
- 例えばSNSの友人取得APIでIDのみ返す場合、クライアントはそのIDを用いてユーザ情報取得を行う必要があり、最低2回のアクセスが必要になってしまい、パフォーマンスに影響が出てしまうため
- APIのユースケースを考えて返すデータを考える
- レスポンスの内容をユーザが選べるようにする
- APIへのアクセス回数は最小限に考えるべきだが、データ量が多すぎた場合、ダウンロードにも処理にも時間がかかってしまうため
- 例えば以下のように利用するデータをクエリパラメータを使って制御できるようにする
http://api.example.com/v1/users/12345?field=name,age
オーバーフェッチやアンダーフェッチはREST APIで問題視されていた。
その解決のために、GraphQLという方法がある。
ある程度、設計が固まっている場合は問題ないが、制御するパラメータが多かったり可変的に変わっていくケースが考えられる場合などは、GraphQLの採用を検討する。
ただし、学習コストが高く歴史も浅いため、開発チームの成熟度も考慮する。
- エンベロープは可能な限り避ける
- HTTPの仕組みにヘッダの概念があり、メタ情報はレスポンスヘッダに記載すべきであるため
- ただし、なんでもフラットなデータ構造にすべきではなく、例えばどちらも同じユーザという構造を表す場合は、階層化し可読性を向上させることも検討する
{
"id": 12345,
"message": "Hello",
"sender": {
"id": 3456,
"name": "Taro Yamada"
},
"receiver": {
"id": 6789,
"name": "Kenji Suzuki"
},
...
}
{
"id": 12345,
"message": "Hello",
"sender_id": 3456,
"sender_name": "Taro Yamada",
"receiver_id": 6789,
"receiver_name": "Kenji Suzuki",
...
}
- 配列をレスポンスとして返す場合も、オブジェクトで包む
- レスポンスデータが何を示しているのかわかりやすくなるため
- レスポンスデータをオブジェクトに統一できるため
- セキュリティ上のリスクを避けることができるため
- JSONインジェクションというブラウザが配列の場合読み込んでしまう仕様を悪用した攻撃を軽減できる
- 配列として取得したデータに続きがある場合は、続きがあるという情報を付与することを考慮する
{
"timelines": [
...
],
hasNext: true
}
データの命名規則
- 多くのAPIで同じ意味に利用されている一般的な単語を用いる
- なるべく少ない単語数で表現する
- 複数の単語を連結する場合、その連結方法はAPI全体を通して統一する
- いろんな意見があるが、特に主張がなければMozillaやGoogleのスタイルガイドに沿ってキャメルケースで統一する
- 変な省略形は極力利用しない
- "timeline"を"tl"、"location"を"lctn"と略さない
- 単数系/複数形に気をつける
- 性別は生物学的な性別はsex、社会的・文化的な性別はgenderで設定する
- 日付のフォーマットはRFC3339を使う
2015-10-12T11:30:22+09:00
ただし、後述するHTTPヘッダで定義されている日付は異なるフォーマット(RFC882/RFC850/ANSI C)で定義されていることに注意する。
- 大きな整数(32ビット、約42億種類以上)を扱う場合は、システムや言語で扱えない場合があるため、その際は文字列で数値を扱うことを考慮する
{
"id": 266031293949698048,
"id_str": "266031293949698048",
...
}
- エラー時にはエラーのステータスコードを返却するだけでなく、ボディにもエラー詳細を返却することを検討する
{
"errors": [
{
"message": "Bad Authentication data",
"code": 215
}
]
}
{
"message": "Not Found",
"documentation_url": "https://developer.github.com/v3"
}
HTTPステータスコード一覧
レスポンスヘッダの先頭に必ず入っている3つの数字であり、処理された結果の概要が示される。
以下のステータスコード表の意味に合わせて、レスポンスヘッダのステータスコードを設計する。
数値1桁での意味合い
ステータスコード | 意味 |
---|---|
100番台 | 情報 |
200番台 | 成功 |
300番台 | リダイレクト |
400番台 | クライアントサイドに起因するエラー |
500番台 | サーバーサイドに起因するエラー |
HTTPの主なステータスコード一覧
処理結果に応じて、以下のステータスコードに当てはまるレスポンスとして返す。
ステータスコード | 名前 | 説明 |
---|---|---|
200 | OK | リクエストは成功した |
201 | Created | リクエストが成功し、新しいリソースが作られた |
202 | Accepted | リクエストは成功した(後続の処理がある、非同期) |
204 | No Content | コンテンツなし(DELETEメソッド成功時に利用を検討するぐらいしか利用用途はない) |
300 | Multiple Choices | 複数のリソースが存在する(データストレージサービスで指定したキーに対して複数データが存在した、など) |
301 | Moved Permanently | リソースは恒久的に移動した |
302 | Found | リクエストしたリソースは一時的に移動している(認証系サービスへのリダイレクトなどで利用) |
303 | See Other | 他を参照(リクエストのメソッドに関わらずGETでリダイレクト) |
304 | NOt Modified | 前回から更新されていない(キャッシュ内容を参照させる) |
307 | Temporary Redirect | リクエストしたリソースは一時的に移動している(POSTからGETヘのリダイレクトを許可しない) |
400 | Bad Request | リクエストが正しくない(他の400番台で表すことができないエラー) |
401 | Unauthorized | 認証が必要(認証エラー) |
403 | Forbidden | アクセスが禁止されている(認可エラー) |
404 | Not Found | 指定したリソースが見つからない |
405 | Method Not Allowed | 指定されたメソッドを使うことができない |
406 | Not Acceptable | Accept関連のヘッダに受理できない内容が含まれている(レスポンスのフォーマットに対応していない) |
408 | Request Timeout | リクエストが時間以内に完了しなかった |
409 | Conflict | リソースが矛盾した(データの登録でIDが重複していたなど) |
410 | Gone | 指定したリソースが消滅した |
413 | Request Entity Too Large | リクエストボディが大きすぎる |
414 | Request-URL Too Long | リクエストされたURLが長すぎる |
415 | Unsupported Media Type | サポートされていないメディアタイプが指定された |
429 | Too Many Requests | リクエスト回数が多すぎる |
500 | Internal Server Error | サーバ側でエラーが発生した |
503 | Service Unavailable | サーバが一時的に停止している |
キャッシュの仕様
Expiration Model(期限切れモデル)
レスポンスデータに保存期限を決めておき、期限が切れた場合に再度アクセスをして取得を行う。
HTTP1.1では以下2種類が用意されている。
Cache-Control: max-age=3600
Expires: Fri, 01 Jan 2016 00:00:00 GMT
同時に設定された場合、Cache-Controlが優先される。Cache-Controlのmax-ageはDataヘッダの時間を参照して計算される。
Validation Model(検証モデル)
今保持しているキャッシュが最新であるかを問い合わせて、データが更新された場合にのみ取得を行う。
条件付きリクエストを行い、更新されていた時のみデータを返し、更新されていなかった場合、304("Not Modified")というステータスコードを返す。
クライアントが保持している情報の状態は、以下の最終更新日付かエンティティタグ(レスポンスデータのハッシュ値)でサーバに伝える。
Last-Modified: Tue, 01 Jul 2014 00:00:00 GMT
ETag: "ff39b31e285573ee373af0d492aca581"
If-Modified-Since: Tue, 01 Jul 2014 00:00:00 GMT
If-None=Match: "ff39b31e285573ee373af0d492aca581"
Heuristic Expiration(発見的期限切れ)
サーバ側で明示的な期限が与えられなかった場合のクライアントがおおよそデータをどのくらい保持するかを決めるための方針。Last-Modifiedを見て最新更新が1年前だったためしばらくキャッシュしても問題ない、といった判断である。
許容できるキャッシュの期限はAPIの性質によるため、基本的にはCache-Controlで制御すべきである。
キャッシュさせたくない場合
以下のヘッダを利用する。
Cache-Control: no-cache
ヘッダを考慮してキャッシュを制御する場合
HTTPのやり取りがプロキシを経由し、そのプロキシがキャッシュ機能を有する場合に用いられる。Varyヘッダを利用することで、どのリクエストヘッダを使ってキャッシュを行うかを制御できる。
例えば、サーバ駆動型コンテントネゴシエーションを実現するケースで、Accept-Languageヘッダを用いて自然言語を設定する場合、以下のような形でリクエストを受け付ける。
Accept-Language: ja
URIだけでキャッシュを判断した場合、Accept-Languageを別でリクエストした際に本来取りたい自然言語の情報を取得できず、jaの情報が取得されてしまう。
レスポンスヘッダに以下のようなキャッシュに利用するヘッダを指定することで、ヘッダを考慮したキャッシュ制御が可能である。
Vary: Accept-Language
ヘッダ設定
Cache-Controlヘッダ
Cache-Control: public, max-age=3600
ディレクティブ名 | 意味 |
---|---|
public | キャッシュはプロキシにおいてユーザーが異なっても共有することができる |
private | キャッシュはユーザーごとに異なる必要がある |
no-cache | キャッシュしたデータは検証モデルによって確認が必要 |
no-store | キャッシュをしてはならない |
no-transform | プロキシサーバはコンテンツのメディアタイプやその他内容を変更してはならない |
must-revalidate | いかなる場合もオリジナルのサーバへの再検証が必要 |
proxy-revalidate | プロキシサーバはオリジナルのサーバへの再検証が必要 |
max-age | データが新鮮である期間を示す |
s-maxage | max-ageと同様だが中継するサーバでのみ利用される |
全員に配信するお知らせ情報や特定地域の転記情報などはpublicでOKだが、/users/meといった自分自身のユーザ情報取得はprivateである必要がある
Content-Typeヘッダ
代表的なメディアタイプ
メディアタイプ | データ形式 |
---|---|
text/plain | プレーンテキスト |
text/html | HTML文書 |
application/xml | XML文書 |
text/css | CSS文書 |
application/javascript | JavaScript |
application/json | JSON文書 |
application/rss+xml | RSSフィード |
application/atom+xml | Atomフィード |
application/octet-stream | バイナリデータ |
application/zip | zipファイル |
image/jpeg | JPEG画像 |
image/png | PNG画像 |
image/svg+xml | SVG画像 |
multipart/form-data | 複数のデータで構成されるウェブフォームデータ |
video/mp4 | MP4動画ファイル |
application/vnd.ms-excel | Excelファイル |
- 正しいメディアタイプを設定するようにする
- 歴史的経緯でtextのタイプがあるが、applicationタイプを利用する方が良い
- application/x-msgpackなどサブタイプに"x-"で始まるものはIANAに登録されていないものであるが推奨されていない
- 自分でメディアタイプを定義する場合、"application/vnd.company.awesomeformat"で設定する
- 利便性だけでなく、セキュリティ面においても正しいメディアタイプを設定すべきである
- 例えばAPIがtext/htmlをメディアタイプで返してしまう場合
- ブラウザで画面上に表示する
- JSONデータに{"data":""}などが入っている
- の条件が揃うと、それが実行されてしまうため
- 例えばAPIがtext/htmlをメディアタイプで返してしまう場合
IANAとは、Internal Assigned Numbers Authorityの略でインターネットに関する番号を管理する組織である。メディアタイプの管理以外にも、ドメインの管理やIPアドレスの割り当ても行なっている。
CORS
異なるオリジンへアクセスする場合、通常はセキュリティ観点でブラウザ上で禁止されている。アクセスしたい場合、プリフライトリクエストを行う必要がある。
※CORSの仕組みは説明すると長くなるのでここでは割愛する。
また別途時間があれば記事を書く。
詳細については以下を参照。
mdn web doc オリジン間リソース共有 (CORS)
X-XSS-Protection
X-XSS-Protection: 1; mode=block
このレスポンスヘッダーがあることでChromeやSafariなどのブラウザが反射型のXSSを発生させそうなパターンをブロックすることができる。
:::message alert
現在はContent-Security-Policy(CSP)を実装することでXSSフィルタリングの必要性が大幅に減少している。現時点では非標準のヘッダになっており、後述のContent-Security-Policyヘッダの使用が推奨される。
https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/CSP
:::
X-Frame-Options
X-Frame-Options: deny
このレスポンスヘッダーがあることで、クリックジャッキングと呼ばれるIFRAMEを他のページに読み込ませて、クリックさせるような攻撃を防ぐことができる。
:::message alert
こちらも現時点ではContent-Security-Policyヘッダのframe-ancestorsディレクティブを使用している同様の機能をより柔軟に実現することが推奨されている。
https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/CSP
:::
Content-Security-Policy
Content-Security-Policy: default-src 'none'
Content-Security-Policy: default-src 'self'
Content-Security-Policy: default-src 'self' example.com *.example.com
IMG要素、SCRIPT要素、LINK要素などの読み込み先としてどこを許可するのかを設定できるヘッダである。XSSやデータインジェクション攻撃などのような、特定の種類の攻撃を検知し、影響を軽減するために追加できるセキュリティレイヤーである。
上記の例のように、コンテンツを特に読み込まない場合や特定のドメインからのコンテンツのみを読み込むなどの制御が可能である。
非常にセキュリティの高い設定ができる一方、制御も大変でありシステム全体に大きな影響を与える可能性がある。本番環境へのスムーズな移行を行うために、report-onlyモードというものがあり、"Content-Security-Policy-Report-Only"ヘッダを設定することで、ブロックは行われず違反があるかの報告のみが行われるため、動作確認にはまずこちらのヘッダの利用を検討すると良い。
Strict-Transport-Security
Strict-Transport-Security: max-age=15768000
このヘッダを利用することで、あるサイトへのブラウザからのアクセスをHTTPSのみに限定させることができる。HTTPの通信が行われた際にHTTPSにリダイレクトする、という手法がよく使われるが、その際の問題として初回が暗号化されていないので、中間車攻撃などによって書き換えられる危険性が高くなっている。このヘッダを設定しておくことで、max-age期間中はブラウザ側で記録してことができ、2回目以降はHTTPで送られてもHTTPSのアクセスに変換される。
Set-Cookie
Set-Cookie: session=e827ea0c...; Path=/; Secure; HttpOnly
ブラウザでセッションを扱う場合にクッキーをセッション管理に使うケースが多い。その際のセキュリティ強化のためにSecure、HttpOnly属性を付与する。
Secure属性を付与するとクッキーはHTTPS時通信のみ、サーバに送り返すようになる。これによりセッション情報は暗号化された通信のみでサーバとやり取りできるようになる。
HttpOnly属性を付与するとクッキーはブラウザ上でJavaScriptなどのスクリプトを使ってアクセスできないようにすることができる。これによりXSSなどによって、そのクッキーに含まれたセッション情報が読み出されることを防止することができる。
X-Requested-With
X-Requested-With: XmlHttpRequest
XSSやXSRF(Cross Site Request Forgery)の攻撃を防ぐ手段の一つとして利用する独自ヘッダである。このヘッダがリクエストに存在しなかった場合、APIを提供するサーバでアクセス拒否する。前の章で紹介したGETメソッドではサーバ側のデータを変化させない設計をしたうえで、同一生成元ポリシーの影響を受けないGETメソッド以外の、FORM要素からのPOSTメソッドのアクセスも防ぐことができる。
(ブラウザからのFORMによるPOSTアクセスはヘッダを独自に追加できないため)
なお、CORSの仕組みを利用してAPIを提供するならば、プリフライトリクエストの際にこのヘッダも明示的に許可する必要がある。
独自のHTTPヘッダを設定する場合
- "X-"という接頭辞をつける習慣があった
- X-GitHub-Request-Id: ...
- x-li-request-id: ...
- 近年ではX-を使うことはRFC6648から、お勧めできない状態であるが、付与されたいた方がわかりやすい意見もあり、議論が難しいものである
- どちらにしても、X-をつけるかつけいないかは、サービスとしては統一する
運用設計
APIのバージョン管理
https://graph.facebook.com/v2.0/me
http;//api.linkedin.com/v1/people
https://api.dropbox.com/a/account/info
APIにはバージョン番号をパスに付与し、互換性がないバージョンアップが発生した場合、別エンドポイントとして提供するようにする。
なお、マイナーバージョンアップは基本的に互換性があることが前提のケースが多いため、パスで管理するバージョンはメジャーバージョンのみで良い。
大量アクセスへの対策
Web上で公開するため、DoS攻撃といった大量のアクセスに備える必要があり、そのためにAPIは以下のような対策を行う必要がある。
- APIは提供するユーザやシステムを特定し、それぞれにアクセス制限を行う
- レートリミットとして1時間に100回などの制限値を設ける
- 制限値を超えた場合、429エラーをレスポンスし、可能であれば、Retry-Afterヘッダで具体的に再アクセスが可能な時間を明示する
- ドキュメントに制限回数を明示したり、API利用者向けのダッシュボードにて情報を提供することも検討する
APIドキュメントの提供
APIのドキュメントは利用者に公開し、バージョンアップなどが発生した場合も反映するようにする。OpenAPIなどのフォーマットを利用すれば、ドキュメントの自動生成も可能で利用するクラウドやフレームワークによっては、API定義ファイルとしても利用できる。
更新が煩雑にならないような仕組みを検討する。
サンドボックスAPIの提供
決済処理などの金銭授受が発生するようなAPIは、本番環境で試すには大きな影響のある。こういったAPIを提供する際はサンドボックス環境を用意し、ユーザがテストを行いやすいようサービスを展開する。
最後に
最後まで読んでいただきありがとうございます。
本の内容を元に一部追記をしたり、記述の構成を変えたりしました。
もちろんこのケースが全てのプロダクトに当てはまるわけではないので、ドメインやサービスの特性、ユーザの環境などを加味したうえで設計方針の採用を検討する必要があると思います。
正直まだまだ奥が深い分野ではありますが、この部分さえ押さえればそこまで悪いAPI設計にはならないと信じています。
それでは。