最近、 仕事では バックエンド側で API (当ページの 文脈では Web API1 を 指す)を 実装し、 それを フロントエンド側で 利用すると いう 行為を ひたすら 続けている。
基本的に よく 使う APIの リファレンス (Stripe API や Shopify Admin API など)や本を 参考に APIを 組み立てている。
しかし、 毎回 参照するのは 非効率的なので 自分用に 備忘録と して まとめる ことにした。
今回ベースと なるのは オライリーから 出版されている 「Web API: The Good Parts 」である。
発売日は 2014年と 古いが Web APIの 性質上、 古くなって 使えないと いう 箇所は ほぼなく、 特に 違和感なく 読み進められる。
基本が 網羅されており、 名著だと 思う。
APIは 単に インタフェースに 過ぎないため、 その 設計に あまり 時間を かけるべきではない、 可能な 限りビジネスロジックの 実装に 時間を 費や したいと 思っていた。
しかし、 最初の 設計を 誤まると 後々 その 負債が 増大していく ことを 体験したので 軽視できない。
この ページでは 今後そういった ことがないように、 自戒を 込めて 書き留めてゆく。
この 章は APIを 扱う上で 基本中の 基本と なる 部分である。
何らかの 開発に 携わっている 人で あれば 自然に 身に 付いている 知識だと 思う。
特に 悩まずに このとおりに 実装すれば 大抵は 問題ない。
LSUDs (Large Set of Unknown Developers)
SSKDs (Small Set of Known Developers)
上記は APIが ターゲットに するのは どんな 開発者なのかと いう ことを 表すときに 使われている 用語である。
APIを 設計する 上では どちらを 対象に するかを 意識しなければならない。
この ページでは 基本的には LSUDsを 対象に 書いているが、 SSKDs向けの 設計方 法に ついても 記載している。
SSKDs向けの 場合は その旨を 記してある。
APIを 設計するに あたり、 クライアントアプリケーションの 画面と その 遷移を まずは 考える。
例えば、 図に して 書き出すと 整理しやすい。 その際は MECE (漏れなく、 ダブりなく)に なるように する。
その図を 元に 必要な 機能を 列挙していく。 これが エンドポイントの 元に なる。
エンドポイントを 設計する ときには 覚えやすく、 どんな 機能を もつ URIなのかが ひと目で わかるように する。
具体的には 以下の 事項を 考慮する。
短く 入力しやすい URI
人間が 呼んで 理解できる URI
省略形を 含まない
国コードなど 標準化された ものは 例外
大文字小文字が 混在していない URI
改造しやすい URI
サーバ側の アーキテクチャが 反映されていない URI
ルールが 統一された URI
ex. IDを パスに 入れるのかクエリパラメータに 入れるのか
URIと メソッドの 関係は、 操作する ものと 操作方 法の 関係である。
1つの URIの エンドポイントに 異なる メソッドで アクセスする ことで、 リソースを どう 扱うかを きちんと 分離できる。
HTMLは Formに おいて POSTと GETしか 使用できない2 。
その 名残の せいなのかAPIに おいても 更新・削除で POSTを 使っている ケースが たまに 見受けられるが、 可能で あれば それ以外 (以下を 参照)も 積極的に 使用したい。
メソッド名 説明 GET リソースの取得 POST リソースの新規登録 PUT 既存リソースの更新 DELETE リソースの削除 PATCH リソースの一部変更 HEAD リソースのメタ情報の取得
※ PUTは 送信した データで 元々の リソースを 置き換える ものであるのに 対し、 PATCHでは その 一部だけを 更新したい 場合に 使用する。
HTMLの Formでは GETと POSTしか 使えないが、 他にも クライアントに よっては HTTPメソッドの 利用が 制限される 場合が ある。
その 場合は API側で GET/POST以外の メソッドを POSTを 使って 表現する ことを 許可できる。
具体的には 2つの 方 法が ある。
まず 1つ目はX-HTTP-Method-Overrideと いう HTTPリクエストヘッダを 使う方法、 2つ目は_methodと いう パラメータを 利用する 方法である。
_methodを 使う 場合はapplication/x-www-form-urlencodedと いう Content Typeで 表される データの 一部として 送信する。
これは Ruby on Railsなどが 採用している 方法である3 。
しかし、 上記の 形式 (application/x-www-form-urlencoded) 以外では データを 送信できない (使い方を 明確に 定義できない) ため、 できればX-HTTP-Method-Overrideを 使用する 方が 良い。
また、 リクエストデータの 中に データ以外の メタ情報が 入ってしまうのは 送信データを 分類すると いう 意味に おいて あまり 好ましくない。
X-HTTP-Method-Overrideを 使う 場合は 以下のように HTTPリクエストヘッダに 利用したい メソッドを 指定するだけ。
X-HTTP-Method-Override: PUT
サーバ側の フレームワークや ミドルウェアに よっては 上記の ヘッダを 自動的に 解釈する 場合も ある ので 有効に 使える 方 法を 選択する。
当然、 フレームワークが 対応していない 場合は 自前で 実装する。
リソースに アクセスする ための エンドポイントを 設計する 際は 以下の 点に 注意する。
複数形の 名詞を 利用する
利用する 単語に 気を つける
スペースや エンコードを 必要と する 文字を 使わない
単語を つなげる 必要が ある 場合は ハイフンを 利用する
絶対と いう 訳ではなく、 プロジェクトに 準ずる
そも そも 単語の つなぎ合わせを 避ける 設計に する
たくさん ある データの 一部を 取得する 際には ページネーションの 仕組みを 利用する。
page/per_pageやoffset/limitのような クエリパラメータを 設定するのが 一般的。
例えば、 数ある アイテムの 中から 101アイテム目から 取得したい 場合は 以下のように する。
per_page=50&page=3
limit=50&offset=100
offset/limitの 方が 自由度は 高いが、 要件に よって どちらに するかを 決める。
ただし、 上記のような 相対的な 取得位置で データを 取得する 方 法には パフォーマンス上の 懸念が ある5 。
また、 更新頻度の 高い データに おいて データの 不整合が 生じると いう 問題も ある ため慎重に 導入を 決めなければならない。
絶対位置で データを 取得する 場合は 相対位置での 問題点を カバーできる。
例えば、 「この IDよりも 前の もの」や 「この 時刻よりも 古い もの」のような 指定方 法を 用いる。
クエリパラメータは その 他に、 絞り込みの ために 使用する。
検索する フィールドが ほぼ 1つに 決まる 場合はqと いう パラメータが 使われる 場合も ある (Googleの 検索結果など)。
クエリパラメータと パスの 使い分けは 一意な リソースを 表すのに 必要な 情報か どうかで 判断する。
これは URIが リソースを 表すものであると いう URIの 思想から きている。
また、 省略可能な 場合は クエリパラメータの 方が 適している。
自分の 情報を 取得したい際は 自分の ユーザIDを いちいち指定するのは 煩雑なのでmeやselfと いった キーワードを パスに 入れて エイリアスと して 利用する (users/me)。
OAuth 2.0 -> 別記事で まとめる。
example.comと いう サービスで APIを 提供する 際の ホスト名は 特に 制約が なければapi.example.comに するのが 適切である。
URIは シンプルで 短ければ 短い ほど 良い。
LSUDs向けの APIではなるべく 汎用的で わかりやすく 使いやすい APIの 設計が 最も 重要である。
SSKDs向けの 場合でも 基本原則は 同じであるが、 エンドユーザに とっての ユーザ体験も 考える 必要が ある。
例えば、 トップページに 新着商品や 人気の 商品、 ユーザ情報などの データを 使う 場合に それぞれAPIを 投げるのは 非効率である。
これは 良い ユーザ体験とは 言えない。 したがって、 こういった 場合では トップページ 表示用APIを 作って それに 1回アクセスするだけで 全ての 情報を 取得できるように する方が 利便性は 高くなる。
な お複数の クライアントアプリケーションに APIを 提供する 場合は ユースケースが 多すぎて 管理が 大変に なってしまう ことは 予測できる。
その 場合は 後述する オーケストレーション層を 挟む方 法が ある。
Martin Fowler 氏に よる 「Richardson Maturity Model 」と いう 記事に よれば、 素晴らしい REST APIに 至る ための 設計レベルには 以下のような ものが ある。
REST LEVEL0 - HTTPを 使っている
REST LEVEL1 - リソースの 概念の 導入
REST LEVEL2 - HTTPの 動詞 (GET/POST/PUT/DELETEなど)の 導入
REST LEVEL3 - HATEOASの 概念の 導入
LEVEL2までは 理解できるが、 LEVEL3の HATEOAS (hypermedia as the engine of application state)とは あまり 聞き馴染みの ない 用語である。
HATEOASとは 設計方 法の ことで、 APIの 返すデータの 中に、 次に 行う 行動、 取得する データ等の URIを リンクと して 含めるように する。
そうする ことで、 その データを 見れば 次に どの エンドポイントに アクセスすれば 良いかが わかる。
面白い 概念であるが HATEOASを 使った APIに 今まで 出会った ことがない。
HATEOASを 使う ことの 最大の メリットは クライアントが あらかじめURIを 知る 必要が ないため、 URIの 変更が しやすくなる 点が あげられる。
まだ、 エンドポイントを 人間が 理解できる 形式に する 必要は ないためセキュリティなどの 観点から URIを 想像しにくい形に できる。
LSUDs向けの APIでは HATEOASの 概念が 全然 広まっていない ことを 考えると この 先使われる ことは ほぼないと 思うが、 SSKDs向けの APIでは ニーズ次第で 採用が 可能かもしれない。
エンドポイントの 設計が 終わったので 続いて リクエストの 結果 返される レスポンスデータを どのように 設定するかに ついて 書き留めてゆく。
データフォーマットに 関しては JSONに デフォルトと して 対応して、 需要や 必要が あれば XMLに 対応する と いうのが 最も 理に 適っている。
XMLの 方が 名前 空間やスキーマ定義の 仕様が きちんと 決まっていたりと JSONに 比べて 表現力は 豊かであるが 大抵は JSONの 構文で シンプルに 表現できるので XMLを 使わなければならない 理由は あまり 存在しない。
その 他に 検討すべき データフォーマットと しては MessagePack が ある。
MessagePackは 効率の 良い バイナリ形式の オブジェクト・シリアライズフォーマットであり、 JSONの 置き換えと して 利用できる。
SSKDs向けの APIで あれば パフォーマンスを 重視して 採用するのも ありかもしれない。
複数の データフォーマットを サポートする 場合は クライアント側から データフォーマットの 形式を 指定させる 必要が ある。
その 場合は 以下の 方 法が 一般的に 使われている。
クエリパラメータを 使う方 法
ex. https://api.kkhys.me/v1/users?format=xml
拡張子を 使う方 法
ex. https://api.kkhys.me/v1/users.json
リクエストヘッダで メディアタイプを 指定する 方 法
リクエストヘッダを 使う 場合はAcceptを 使用する。
3つの うちどれを 使うかだが、 メディアタイプを 指定する 方 法が HTTPの 仕様を 最大限活用しており、 最も お行儀は 良い。
ただ、 LSUDs向けAPIの 場合は 多少ハードルが 高いので クエリパラメータの 指定方 法も サポートすると 良い。
JSONP (JSON with Padding)とは JSONに それを ラップする JavaScriptを 付け加えた ものを 指す ( その ため、 そも そも JSONではなく JavaScript)。
例えば 以下のような データ。
callback({ " id " : 123 , " name " : " kkhys " })
この JavaScriptを script要素で 読み込むと、 データが 読み込まれた 際にcallbackと いう 関数が 呼び出され、 引数の データが 渡される。
callback関数は JSONPを 呼び出した script要素の 存在する ページに あらかじめ用意しておく 必要が ある。
現在は クロスオリジンで 通信が 可能な 環境が 整った ため 使われる ことは ほぼないし、 サポートする 必要も ないと 思うが、 なぜJSONPが 流行ったのかに ついては 知っておく 必要が ある。
その 理由と して XMLHttpRequest では 同一オリジンポリシー の 制限に よって、 同じ オリジンへの アクセスしか 行うことができないからである。
しかし、 script要素は 同一オリジンポリシーの 規制の 対象外の ため、 JSONを script要素を 使って JavaScriptと して 読み込めば、 ドメインを 超えた アクセスが 可能に なる。
いわば、 規制を 回避する ために 苦肉の 策で 生み出された テクニックである。
APIで 返すレスポンスデータを 決定する 際に まず 考えるべきことは、 APIの アクセス回数が なるべく 減るように する こと である (→ Chatty APIを 避ける)。
APIの アクセス回数が 増えると HTTPの オーバーヘッドが 上がり、 アプリケーションの 速度が 低下する。
まだ、 サーバ側の 負荷も 増加してしまうなど メリットは 何もない。
APIの バックエンドの データ構造から 考えると テーブルの 内容を ただ そのまま 返すだけと いうのは 何らかの 問題が ある 可能性が ある ため設計の 見直しを 行う。
Web APIは 単なる データベースの アクセスインタフェースではなく、 アプリケーションの インタフェースである と いう ことを 意識する。
ただ、 APIの アクセス回数を 減ら すために できる 限り 多くの データを 返すように すると、 今度は 必要以上に 大量の データを クライアントが 受け取らなければならなくなってしまう。
その 場合は 取得する 項目を ユーザが 選択可能に する 方 法を 採用する。
例えば 以下のような クエリパラメータを フィールドと して 設定可能に する。
https://api.kkhys.me/v1/users/1?fields=name,age
エンベロープとは APIの データ構造の 文脈で 言うと、 全ての データを 同じ 構造で 包むことを 言う。
例えば、 APIレスポンスに ステータスなどの メタデータを エンベロープと して 含むような APIは 実際に あるかもしれないが、 これは 冗長なのでやるべきではない。
なぜなら HTTP自体が エンベロープの 役割を 果たしているからである。
メタデータを レスポンスに 含みたければ HTTPヘッダに 入れて 返せば 良い。
メタデータ以外の データでも 可能な 限り階層構造を 使って 表さずに フラットな 状態と して 返すほうが JSONの データサイズは 小さくなる ため不要な 階層化は するべきではない。
ただし、 例外と して APIで 配列を 返したい 場合は レスポンス全体を オブジェクトに して その 中に 配列を 入れる。
そうする ことで レスポンスデータが 何を 示している ものかが わかりやすくなるし、 レスポンスデータを オブジェクトに 統一できる。
また、 トップレベルが 配列である JSONは JSONインジェクション6 の リスクが 大きくなる ことを 考えると 常に オブジェクトで 返す方が 良い。
配列を 返すデータでは 絶対位置での 指定を 行う 設定に する 場合が ある。
その 場合に データが 全体で 何件あるかと いう 情報を 使う 場合も 多いが、 処理が 重くなりがちなので 本当に 必要か どうかは 検討しなければならない。
そして、 全件数を 取得しない 場合は ページネーションで 取得する ことに なるが、 その 場合は 今取得した データには 続きが あるのかと いう 情報が 必要に なる。
よく ある 手法ではhasNextと 言う パラメータを 返すパターンが ある。
また、nextPageToken (Googleの APIで よく 見る)のように 次の ページを 取得する ための トークンを 実装する 方 法も ある。
各データ項目の 名前は 以下の 考え方に 基づいて 命名する。
多くの APIで 同じ 意味に 利用されている 一般的な 単語を 用いる
なるべく 少ない 単語数で 表現する
ex. registrationDateTime → registeredAt
複数の 単語を 連結する 場合、 その 連結方 法は API全体を 通して 統一する
キャメルケースorスネークケース
JavaScriptで 利用する ことを 考えると キャメルケースの 方が 適切
変な 省略形は 極力利用しない
単数形/複数形に 気を 付ける
sex → 生物学的な 性別
gender → 社会的・ 文 化的性別
上記の 定義からも わかるように、 医療形の サービスのような 生物学的な 性別が 必要な 場合はsexを 使い、 それ以外の サービスはgenderを 使う。
基本的には LSUDsを ターゲットに した APIを 作成する 場合は RFC 3339 を 使えば 良い。
RFC3339は 事実上、 ISO 8601 の サブセットである。 いまいち違いが 分かりづらい 場合は 以下の サイトが 参考に なる。
私たちがISO 8601と して 使っている ものは 大抵RFC 3339にも 含まれている。
SSKDs向けの APIを 作成する 場合は 比較や 保持が 非常に 容易で サイズも 小さい Unixタイムスタンプ (epoch秒)を 使うと いう 手も ある。
SQLに おける 通常の 整数 (int/integer)では 32ビットの 大きさを 扱える。
つまり、 integerで 数を 表す 場合は 最大42億の 数字を 表せるが、 Facebookや Xなど 億単位の ユーザを 抱える サービスで 連番を 降っていく と 32ビット整数では 扱えなくなってしまう。
ちなみに Xは 2013年の 段階で 64ビット整数に 移行している 7 (参考 )
JavaScriptでは 数値を 全て IEEE 754 規格に 基づいた 64ビット浮動小数と して 扱う。
その ため、 大きな 整数を 扱うと 誤差が 出る ため注意が 必要。
したがって、 そのような 巨大な 数を 扱う 場合は 文字列と して 返す ことで 問題を 回避できる。
例えば、 Xの 場合はidの ほかにid_strと いう 文字列を 返すようになっている。
エラーを 返す際に 色々 考えるべきことは あるが 必ずしなければならないのは 適切な ステータスコードを 返す ことである。
ステータスコード 意味 100番台 情報 200番台 成功 300番台 リダイレクト 400番台 クライアント最後に起因するエラー 500番台 サーバサイドに起因するエラー
データと して エラー情報が 返ってくるが 200番台の ステータスコードが 返ってくると いう ことは ありえない。
とは いえ、 ステータスコードだけ 返ってきても 何が 原因で エラーが 発生したのかが ユーザは わからないためデバッグの 手助けに なるような 情報を 提供する。
エラーの 内容を 返す方 法には 2つ あり、 1つは HTTPレスポンスヘッダに 入れて返す方 法、 2つ目は レスポンスボディに 入れて返す方法である。
クライアント側の 利便性を 考えると レスポンスボディに データを 入れる 方 法が 良いかもしれない (実際、 大抵の APIは レスポンスボディに エラーデータが 入っている)。
エラー詳細情報には API提供側で 独自の ステータスコードを 定義すると 利用者側で 分かりやすい。
また、 エラー原因を 開発者が 調べられるような メッセージや URLが あるとさらに 親切。
極力避けるべきだが どうしても メンテナンスを 実施しなければならず、 サービスを 停止する 場合が あるかもしれない。
その 場合は ステータスコードと して 503を 返し、 HTTPヘッダには Retry-After を 使っていつメンテナンスが 終わるのかを 示しておく。
これを 知っておけば クライアントの 実装を する ときも メンテナンスが 終わってから APIを リクエストするように 実装できる。
基本的に エラー内容は なるべく 具体的に 正確に 返すべきであるが、 ログイン処理など セキュリティが 絡む箇所では 曖昧な方が 良い。
Web APIは HTTP上で 通信を 行うので、 HTTPの 仕様を 理解して それを 活用した 方が より 使い勝手の 良い ものとなる。
ある 意味、 この ページの 章で 最も 大切な 部分かもしれない。
200番台の ステータスコードを 返す際は ただ200を 返すだけではなく メソッドに よって 使い分ける。
例えば、 GETや PUT, PATCHの 場合は 200とともに 操作した データを 返し、 POSTの 場合は 201を 返す。
DELETEした 場合は 特に データを 返す必要は ないため、 No Contentを 表す204を 返す。
ただ、 どの データや ステータスを 返すかには 諸説ある ためAPI全体で 統一する ことが 重要に なる。
300番台の ステータスコードは リダイレクトを 伝える ために 利用する ステータスコードである。
APIの 場合は ウェブサイトのように URIの 変更や サイトの 移転に 伴うリダイレクトを 行う ことは あまり 好ましくない。
なぜなら、 リダイレクトを どのように 行うかは クライアントの 実装次第であり、 将来起こるか 起こらないかわからない リダイレクトを クライアント側が 実装している 可能性は あまりないからである。
クライアントの リクエストに 問題が あった 場合は 400番台の ステータスコードを 返す。
ステータスコード 名前 説明 400 Bad Request リクエストが正しくない 401 Unauthorized アクセスが禁止されている 403 Forbidden 認証が必要 404 Not Found 指定したリソースが見つからない 405 Method Not Allowed 指定されたメソッドは使うことができない 406 Not Acceptable Accept関連のヘッダに受理できない内容が含まれている 408 Request Timeout リクエストが時間以内に完了しなかった 409 Conflict リソースが矛盾した 410 Gone 指定したリソースは消滅した 413 Request Entity Too Large リクエストボディが大きすぎる 414 Request-URI Too Long リクエストされたURIが長すぎる 415 Unsupported Media type サポートしていないメディアタイプが指定された 429 Too Many Requests リクエスト回数が多すぎる
API開発を 行う際は 最低でも 上記の 不正な リクエストに 対応する ステータスコードと エラーメッセージを 返すように して おきたい (大抵は 時間が なくて 400エラーと して まとめてしまうけれども……)。
500番台の ステータスコードは サーバ側に 問題が あった 際に 返される。
発生しないに 越した ことは ないが、 どうしても バグが 混入してしまう 場合も ある ため、 その 場合は ログと 管理者への 通知を 行い 再発を 防止する。
HTTPの キャッシュには 以下の 2つの タイプが ある。
Expiration Model (期限切れモデル)
あらかじめレスポンスデータに 保存期限を 決めて おき、 期限が 切れたら 再度アクセスを して 取得を 行う
Validation Model (検証モデル)
今 保持している キャッシュが 最新であるかを 問い合わせて、 データが 更新されていた 場合に のみ 取得を 行う
キャッシュが 利用可能な 状態を HTTPではfresh、そうでない 場合はstaleと 呼ぶ。
期限切れモデルの 場合は 保存期限を レスポンスに 返すが、 その際に 2つの 方 法が ある。
1つは Cache-Control レスポンスヘッダを 使う方法、 もう 1つは Expires レスポンスヘッダを 使う方法である。
この どちらかを 使うかは、 返すデータの 性質に よる。
例えば、 天気予報のような 毎日 同じ 時間に 更新される 場合は Expiresで その 日時を 指定できる。
また、 今後 更新される 可能性が ない データや 静的データの 場合は 遠い 将来の 日時を 指定する ことで、 一度 取得した キャッシュを ずっと 保存しておける。
一方、 Cache-Controlは 定期更新ではない ものの 更新頻度が ある 程度限られている ものや、 リアルタ イム性が それほど 重要でない 情報を 返す 場合に 用いられる。
max-ageの 計算には Date ヘッダを 利用する。
Dateヘッダは HTTPの 仕様に より、 いく つかの 例外を 除いて 必ず 付けなければならない。
その際は RFC 1123 を 使用する。
その 他の Cache-Controlの ディレクティブは こちら を 参照。
期限切れモデルが レスポンスを 受け取った 時の 情報だけを 元に キャッシュの 保持時間を 決めて いたのに 対して、 検証モデルは 今 持っている キャッシュが 有効か どうかを サーバに 問い合わせる。
期限切れモデルは 期限が 切れるまで ネットワークアクセスが 発生しないのに 対して、 検証モデルは キャッシュチェックの 際にも ネットワークアクセスが 発生してしまう。
ただ、 大きな データで あれば 期限切れモデルと 比べた 際に 検証モデルの 方が 優位性が ある。
検証モデルを 行うには、 条件付きリクエストに 対応する 必要が ある。
その 際にはLast-Modified (最終更新日時)とETag (任意の 文字列)と いう レスポンスヘッダを 使って レスポンスを 返す。
サーバ側では 送られてきた 情報と 現在の 情報を チェックし、 変更が なければ 304ステータスと 空の レスポンスボディを 返し、 変更が あれば 通常通り200ステータスと 更新された レスポンスボディを 返す。
サーバ側が 明示的な 期限を 与えなかった 場合は クライアント (ブラウザ)が 有効期限を 推測・算出する。
この ことを Heuristic Expiration (発見的期限切れ)と 言う。
キャッシュコントロールに ついて 一番 理解しているのは サーバ側であるのは 間違いないので どれくらい キャッシュを すべきかと いう 情報は レスポンスに 含むように する。
キャッシュを させたくない 場合はCache-Control: no-cacheを 指定する。
な お、no-cacheは 厳密には キャッシュを しないと いう 意味ではなく、 最低限 「検証モデルを 用いて 必ず 検証を 行う」必要が ある ことを 意味している 点に 注意が 必要。
機密情報などを 含むデータを 中継する プロキシサーバに 保存して ほしくない 場合はno-storeを 指定する。
また、 キャッシュを 行う際は 場合に よっては Vary ヘッダを 使用する。
HTTPには、 Acceptで 始まる 一連の リクエストヘッダの 値に よって レスポンスの 内容を 変更する 仕組みが ある。
この 仕組みは コンテンツネゴシエーション と 呼ばれている。
コンテンツネゴシエーションを 使う 場合は 同じ URIでも ヘッダの 値に よっては 内容が 同一ではなくなる ため、 URIだけを 見て キャッシュすると 本来取得すべき データを 取得できなくなってしまう。
そういった ときに Varyヘッダを 使えばどの リクエストヘッダで 生成された 内容を キャッシュするか どうかを 指定できる。
HTTPの リクエスト、 レスポンスでは 送信する データ本体の 形式を 表すために メディアタイプを 指定する 必要が ある。
例えば、 レスポンスデータが JSONである 場合は、application/jsonを 指定する。
その 他の MIMEタイプは こちら を 参照。
CORS を 行うには、 まずクライアント側から Origin と いう リクエストヘッダを 送る 必要が ある。
この ヘッダには アクセス元と なる 生成元を 指定する。
例えば 以下のような リクエストを 行う 場合。
Origin: https://kkhys.meを リクエストヘッダに 追加する。
サーバ側では あらかじめアクセスを 許可する 生成元の 一覧を 保持しておいて、Origin ヘッダで 送られてきた 生成元が その 一覧に 含まれているか どうかを チェックする。
もし、 そこに 含まれていない 場合は 403エラーを 返す。
もし、 一覧に 含まれている 場合は、 Access-Control-Allow-Origin にOrigin ヘッダと 同じ 生成元を 入れて返す ことで、 アクセスが 許可された ことを 示す。
仮に アクセスしたリソースが セキュリティ上どこの ページから 読み込まれても 問題ない 場合はAccess-Control-Allow-Origin ヘッダに*を 指定する。
CORSには プリフライトリクエスト と いう 特別な サーバへの 問い 合わせ方 法が 定義されている。
プリフライトリクエストを 行う ことで 生成元を またいだリクエストを 行う前に そのリクエストが 受け入れられるか どうかを 事前に チェックできる。
CORSに 対応した ブラウザでは 単純リクエスト ではない 場合、 プリフライトリクエストを 自動的に 行う。
また、 CORSでは ユーザ認証情報を 送信する 際は Access-Control-Allow-Credentials ヘッダを レスポンスに 含まなければならない 点に 注意が 必要である。
これが なかった 場合は ブラウザが 受け取った レスポンスを 拒否してしまう。
Web APIは 通常の サービスと 同様に 一度 公開されたらずっと 同じと いう 訳には いかず、 様々な 状況に 応じて 変化していく ものである。
その 際に データの 形式 その ものが 変わる 場合も 考えられる。
こうした APIの 変更は 非常に 大変な ため、 あらかじめAPIを 設計する 段階から 考えておかなければならない。
例えば、 APIの 仕様が 突然変わると その APIを 使っている サービスは 途端に エラーを 吐いて 動かなくなってしまう。
自分たちの サービスで 使っている APIであっても ブラウザの キャッシュの 問題などが ある ため、 いきなり破壊的変更を 実施する ことは 危険だ。
最も 良い方 法は 一度 公開した APIを できる 限り 変更しない ことである。
つまり、 新しい APIを 別の エンドポイントで 公開すれば 良い。
そうする ことで 古い 形式で アクセスしてきている クライアントに 対しては それまでと 変わらない データを 送り、 新しい 形式での アクセスには 新しい 形式の データを 返せる。
バージョン番号は クエリパラメータに 設定したり、 メディアタイプで 指定する 方 法など 色々 あるが、 最も よく 利用されている URIの パスに バージョンを 入れる 方 法が 良さそう。
以下が 例。
https://api.kkhys.me/v1/users
v1が バージョン番号である。
バージョンを 増やすほど メンテナンスコストが かかる ため無闇に 増やしてはいけない。
後方 互換性を 保つことが 可能な 変更は 可能な 限り 同じ バージ ョンでの マイナーバージョンアップで 対応する。
どうしても メジャーバージョンを アップグレードしたい 場合は 継続的な 告知や Blackout Test (一時的に APIを アクセス不可に する テスト)を 行う8 。
APIが 公開終了した 際には ステータスコード410 (Gone)を 返すように しておく。
さらに 公開終了した 旨を 知らせる エラーメッセージと それらに ついての 説明を APIドキュメントに 書いておくと ユーザから すれば ありが たい。
そうする ことで クライアント側で 410が 返ってきた ときの 処理を あらかじめ書いておける (スマートフォンの 実装で あれば 強制アップデートさせるなど)。
また、 利用規約には サポート期限を 明記しておくとより 信頼性は 高まる。
LSUDs向けの APIは なるべく 汎用性の ある 設計に する ことが 求められる。
その ため、 1つの アクションを 行うのに 複数の APIに アクセスしなければならなかったり、 不要な データも 受け取らなければならなかったりするが、 これは ある 程度仕方 ない ことである。
このような APIを 洋服の フリーサイズに なぞらえて one-size-fits-all (OSFA) アプローチと 呼ぶ 人も いる。
一方で SSKDs向けの APIは そういった 汎用性に 縛られる ことはない。
その 利用者の ユースケースに 合わせた 使いやすい APIを 提供できる。
ただ、 使い方が 1つではなく いくつにも 別れている 場合は それぞれに 合わせて APIを 用意したり、 維持する ことは 大変に なってくる。
その 場合は サーバ側の 汎用的な APIと クライアントの 間に オーケストレーション層を 挟むことで 様々な ユースケースに 対応できる。
BFF (Backend For Frontend) アーキテクチャで 検索すると 色々と 事例が 出てくるので 参考に する。
Web APIは HTTPを 通じて 公開される サービスであるので、 通常の アプリケーションと 同様に 安定性や セキュリティが 要求される。
また、 Webアプリケーションとは 異なり、 機械的な アクセスを 受け入れる ことを 前提と している ため、 通常の アプリケーションとは 異なる 対策が 必要に なる。
当然の ことだが HTTPに よる 通信は TLSに よって 暗号化しなければならない。
HTTPSを 使う ことで、 APIの やり取りの 内容だけではなく、 エンドポイント、 ヘッダに 含められて 送られる セッション情報など 全て 暗号化される ためセッションハイジャックなどの ハッキングが 行われる 危険性を 大幅に 減らせる。
ただし、 HTTPSを 使えば 必ずしも 安全と いうわけではない。
HTTPSに よる 通信を 行う 場合は サーバが 送ってきた SSLサーバ証明証を 正しい ものかどうか検証する 必要が ある。
それを 確かめていない 場合、 中間者攻撃 (MitM )に よる 盗聴などが 行われる 危険性が ある。
とは いえ、 これらは クライアント側の 問題であるから サーバ側で いくら対策を しようが 対応が 難しい 問題である。
しかし、 HTTPSへの 対応に よる デメリットは ほぼないため (ハンドシェイクに 時間が かかるなどは ある) 必ず 対応しておけば 問題ない。
XSS (Cross Site Scripting)とは ユーザの 入力を 受け取って それを ページの HTMLに 埋め込んで 表示する 際に、 攻撃者から 送られてきた 悪意の ある JavaScriptなどを 実行させてしまう 攻撃の ことである。
XSSは Webアプリケーションだけではなく APIでも 実行され得る ため対策が 必要に なる。
したがって ユーザからの 入力は どのような 使われ方を するにしても チェックが 必要であるし¸、 レスポンスの 際にも データの 内容を チェックして、 異常な 値は 削除しなければならない。
これらは Webアプリケーションを 構築する 場合は 当然考慮しなければならない ことだが、 JSONなどの 形式で データを 返す 場合は ブラウザの 挙動に よって 発生してしまう XSSが 存在する ため注意する。
例えば、 JSONを 返すAPIのContent-Typeの 値がtext/html だった 場合、 この JSONを 返すURIに 直接ブラウザで アクセスすると、 この データは HTMLと して 解釈される ためscript要素に 含まれる JavaScriptが 実行されてしまう。
これを 防ぐ ためにはContent-Typeにapplication/jsonを 指定すれば 良い。
ただ、 これだけでは 十分ではない。
なぜなら、 IE (今は 対応は 不要だと 思うが)にはContent-Typeを 無視して、 データの 内容から データ形式を 推測する Content Sniffing と いう 機能が あるからである。
この機能を 無効化する ためには 以下の レスポンスヘッダを 付与する。
X-Content-Type-Options: nosniff
この ヘッダを 付ける ことで Content Sniffingが 行われなくなり、Content-Typeで 指定された メディアタイプと して 解釈されるようになる。
JSONインジェクションの 危険性を 減らすことにも 繋がる ためJSONを レスポンスで 返す際は 必ず指定する。
X-Content-Type-Optionsに 対応していない ブラウザでは 追加の リクエストヘッダの チェックと JSON文字列の エスケープが 必要であるが、 そのような ブラウザは 現在ないため割愛する。
XSRF (Cross Site Request Forgery)とは ユーザが 悪意の ある ページに アクセスした 際に、 その 中に 埋め込まれた リンクなどを 経由して、 全く 別の サイトへの リクエストが 行われ、 それに よって その ユーザの 意図しない 処理が 行われる ことである。
APIに おいて XSRFを 避ける ための 方法は Webアプリケーションと 大きくは 変わらない。
まず 1つ目は サーバ側の データが 変化するような アクセスに 関しては GETメソッドを 利用しないと いう ことである。
これに より img要素などに 攻撃用の コードを 埋め込むことができなくなる。
2つ目は、 これが 一般的な 方 法だが、 XSRFトークンを 使う方法である。
送信元と なる 正規の フォームに、 その サイトが 発行した トークンを 埋め込んで おき、 その トークンが ない アクセスは 拒否すると いう ものだ。
APIに 関しても 同様の 対策を 適用し、 アクセスの 際に XSRFトークンを 渡して それを パラメータに 含まれていない 場合は アクセスを 拒否できる。
ただし、 この 方 法は やや 煩雑なのでX-Requested-Withのような 特別な リクエストヘッダを 付与して、 その ヘッダが なければ アクセスを 拒否する 方 法が 手軽かもしれない9 。
広く 一般に 公開している APIで あれば ユーザは APIが 行う HTTP通信を 見る ことができる。
ユーザの 中には、 サーバに 対して本来の 使われ方とは 違う アクセスを 行う ことで サーバの 脆弱性を 突いて 自分に 有利な 情報や 状況を 作り出そうと 企てる 者が いても おかしくはない (ここでは ハッカーなどの 第三者ではなく 正常に 登録している 利用者を 指す)。
最も シンプルな 不正アクセスの 方法は パラメータの 改ざんである。
パラメータを 改ざんする ことで 本来見る ことのできない データを 取得できたり、 サーバ側の データを 本来なら あり得ない 値に 変更できたりも する。
こうした ことを 避けるには 当たり前の ことだが、 クライアント側だけではなく、 サーバ側できちんと バリデーションを 行うしかない。
テスト漏れなどで 抜け穴が あるにも かかわらず 公開されている サービスは 脆弱性が 見つかっていないだけで 実際は 多い (と 思っている)。
そうなった ときの ダメージは 計り知れないため注意を きちんと しておく。
次に 気を つかなければならないのは リクエストの 再送信である。
一度 送信したリクエストを 再送信する ことで、 同じ 処理を サーバ側に もう 一度させてしまうと 場合に よっては 大問題に なることもある。
対策と しては APIごとに 繰り返しアクセスされる ことに よって 問題が 発生するのかを 判断して、 発生する 可能性が ある ものに 関しては 状態を 管理して 同じ アクセスが あった 場合は エラーに するなどの 処理が 必要に なる。
大量の アクセスを 受けると、 サーバの リソースは その アクセスを 捌く ために 力を 注が ざるを 得なくなり、 やがて その 不可に 耐えられなくなってしまう 場合も ある。
そうすると、 大量の アクセスを 行った アクセス元だけではなく、 関係ない ユーザも 全く サーバに 接続できなくなってしまう (いわゆる Dos攻撃 )。
一般に 公開している APIで あれば Dos攻撃のような 悪意を 持った ハッカーからだけではなく、 善良だが 未熟な 開発者が 不注意で 無限ループを 書いてしまい 結果 的に Dos攻撃に なってしまう ケースも 考えられる。
そうならないための 最も 現実的な 方法は ユーザごとの アクセス数を 制限する ことである。
その際は 以下のような ことを 決める 必要が ある。
何を 使って ユーザを 識別するか
リミット値を いくつに するか
どう いう 単位で リミット値を 設定するか
リミットの リセットを どう いう タイミングで 行うか
アクセス回数を 制限する 期間の 開始時間を 毎時 0分のように 決まった 時間に するか、 最初に APIに アクセスした タイミングに するか
仮に レートリミットを 超えてしまった 場合は レートリミットの 情報と 共に ステータスコード429を 返せば 良い。
そして、 その ときにはRetry-After ヘッダを 使って 次の リクエストを するまでに どれくらい 待てば良いかを 指定すると 親切。
ちゃんとした 大規模な サービス (Googleなど)で あれば レートリミットだけを 返す専用の APIが あったりする。
余裕で あれば そういった 便利な APIを 実装してみても 良いかもしれない。
Web API その ものを 構築する 以外に、 その APIを 利用者が より 便利に 利用できるように やっておいた 方が 良いことがいくつか ある。
APIドキュメントの 提供
サンドボックスAPIの 提供
いわば 実験用の 環境。 金銭が 絡む場合は あった方が 良い
APIコンソール
SDKの 提供
メンテナンスコストが 大きく 上がる ため導入する 際は 注意
こうやって まとめてみると 結局は HTTPの 仕様に ついてもっと 知る 必要が あると 感じた。
CDNや キャッシュ関係の 知識が 浅いので 改めて 見直してみよう。