API 設計

自分のノートに『Web API: The Good Parts』を読んで理解した内容を織り込んだもの。 本文の引用はしていない。書籍由来の箇所に注記が無いため、どこが自分の判断で どこが書籍かは分けられていない。書籍側だけを読むなら Web API 設計の要点。 HTTP メソッド・ステータスコード・ヘッダの仕様そのものは RFC に基づく事実。

このディレクトリ


何を API 化すべきか

そのサービスができることを、すべて API 経由で行えるようにする。 EC サイトなら商品の検索と購入、写真共有サイトなら投稿とタグ付け。

より正確には、そのサービスが価値を生み出しているコアの部分をすべて公開する。 逆に、コアでない部分 — 家計簿サービスが外部から買っているデータでの通貨変換のような機能 — を単体の API として出すことにはあまり意味がない。それはそのサービス固有のものではないから。

エンドポイントの基本的な設計

URI

目指す状態は 「覚えやすく、どんな機能を持つ URI なのかがひと目で分かる」。 長い URI はたいてい、不要な情報が入っているか意味が重複している。

ホスト名は api.example.com
ホスト名の次はバージョン(v2 等)
バージョンは後方互換性を失う大きな変更を行うときだけ上げる
基本はすべて小文字を使う
標準化された単語以外は極力省略しない
複数のAPIを見比べて、最もよく使われている・最もしっくりくる単語を選ぶ
セキュリティの観点から、サーバー側が何で実装されているか分からないようにする
リソースは複数形にする
一意なリソースを表すのに必要な情報はパス、省略可能なパラメーターはクエリパラメーター
動詞は極力エンドポイントに入れない
パーセントエンコーディングされた文字を入れない
単語をつなげる必要がある場合はハイフンを使う
検索のためのエンドポイントには search を入れる
oauth2 認証用のパスは /oauth2/token 等

補足として押さえておくこと。

  • 省略形は避ける。 svu は読んでも何のことか確信が持てない。 ただし国コードの jp jpn のように ISO 3166 で標準化されてコード体系になっているものは別で、 むしろ独自表記より分かりやすい
  • サーバー側のアーキテクチャが透けないようにする。 /cgi-bin/get_user.php?user=100 のような URI は、PHP で CGI として動いていることを教えてしまう
  • ルールを URI 全体で統一する。 一方は /friends?id=100 と複数形+クエリ、もう一方は /friend/100/message と単数形+パス、 という混在をしない
  • 複数形にする理由は、DB のテーブル名と同じで usersfriends が「集合」を表しているから
  • 単語の連結にハイフンを使うのは、ホスト名がハイフンを許可しアンダースコアを許可しないため。 URI 全体をホスト名と同じルールで統一するとハイフンに揃う
  • search を入れるのは「これは検索用であって全件取得用ではない」と示すため
  • オープンな API と、提携先限定などクローズドな API は、エンドポイントを分けて公開する

バージョンを上げる基準

バージョンは頻繁に上げない。 複数バージョンを保守するのはコストが高く、利用者から見ても分かりにくい。

  • 小さな変更は後方互換性を保って対応する(マイナーバージョン以下の変更で吸収する)
  • 後方互換性を失ってよいと判断できるほどの大きな変更のときだけ上げる

だからメジャーバージョンで表すのが妥当ということになる。

HTTP メソッド

URI とメソッドの関係は、操作する対象と操作方法の関係。 URI の R は Resource で、リソースは何らかのデータを表す。 つまり URI が「何を」、HTTP メソッドが「どうするか」

メソッド名説明
GETリソースの取得
POSTリソースの新規登録
PUT既存リソースの更新
DELETEリソースの削除
PATCHリソースの一部更新
HEADリソースのメタ情報取得
  • GET でサーバー上のリソースが変わることは基本的にない。 既読/未読や最終アクセス日時のように、参照されたこと自体を記録する場合は例外
  • **POST は「その URI の配下に新しいデータを作る」**イメージ
  • PUT は更新対象の URI そのものを指定して、内容を完全に上書きする
  • HTTP の定義上は PUT で新規作成もできるが、 Web API では修正に PUT、新規作成に POST を使うのが一般的

メソッドが使えない場合の代替

HTML の Form は GET と POST しかサポートしていない。 クライアント側のライブラリが GET/POST にしか対応していないこともある。

その場合は POST を使い、本当は何のメソッドを使いたいのかをメタ情報としてサーバーに送る。 方法は2つ。

  • X-HTTP-Method-Override リクエストヘッダを使う
  • _method パラメータを使う。 Form のパラメータの1つとして application/x-www-form-urlencoded で送る。Rails などが採用している

一覧に list は要らない

一覧のエンドポイントを設計していると list を付けたくなるが、なくても一覧だと意味は通じるし、 URI も短くなる。不要なら取り去ってよい。

クエリパラメーター

  • 取得位置と件数は offset / limit offset/limit の方が自由度が高く、利用者にとって使いやすい
  • 全文検索は q Google の検索もクエリパラメータ名は q

認証

OAuth は、あるサービスに対して、ユーザーが別サービス(Facebook 等)に登録している情報の 利用を許可できる仕組み

要点は、利用する側のサービスに Facebook のパスワードを入力しなくてよいこと。 認証に成功するとアクセストークンを受け取り、 認可された情報にだけアクセスできるようになる。

Resource Owner Password Credentials で認証する場合、 エンドポイントへは application/x-www-form-urlencoded、文字コードは UTF-8 で送る。

POST /v1/oauth2/token HTTP/1.1
Host: api.example.com
Authorization: Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=takaaki&password=abcde&scope=api

Authorization ヘッダはクライアント認証で、 アクセスしてきたサービスやアプリケーションが何であるかを特定するための情報。

  • アプリを登録すると発行される ClientID と ClientSecret を、 ユーザー名/パスワードとみなして Basic 認証の形式で Base64 変換したものが入る
  • 利用は任意だが、入れておくとアプリごとにアクセス数を制限したり、 許可していないアプリをブロックしたりできる
  • Authorization ヘッダではなく、client_idclient_secret をリクエストボディに入れることもできる
  • scope はアクセスのスコープ指定(省略可能)
 {
    "access_token": "b77yz37w7kzy8v5fuga6zz93",
    "token_type": "bearer",
    "expires_in": 2629743,
    "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
  }
  • token_typebearerRFC 6750 で定義された OAuth 2.0 用のトークン形式。 以後は Authorization: Bearer b77yz37w7kzy8v5fuga6zz93 の形で送る
  • リフレッシュトークンは、アクセストークンを再発行してもらうための別のトークンで、 アクセストークンと同時に取得できる
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
    "error":"invalid_token"
}

再発行のリクエストは grant_typerefresh_token を指定して、リフレッシュトークンと一緒に送る。

POST /v1/oauth2/token HTTP/1.1
Host: api.example.com
Authorization: Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA

レスポンス

ボディ

目安は 1スクリーン1 API コール、1セーブ1 API コール

友人一覧が ID の配列だけを返すと、クライアントは ID ごとにもう一度問い合わせることになる。 複数ユーザーをまとめて取れる API があっても、最低2往復が必要になってしまう。

DB の各テーブルの中身をそのまま返すだけの API は、たいてい使い勝手が悪い。 テーブルに ID しか入っていないとしても、それをそのまま返せばよいわけではない。 ユースケースを考えて、利用者が最もシンプルに扱える形を設計する。

ユースケースに沿った形で、できるかぎり少ないアクセス回数ですむAPI設計を心がける
取得する項目を利用者が選択可能にする(fieldsパラメーター)
HTTPのレスポンスボディは実際のデータのみを返す
レスポンスデータをオブジェクトに統一する
レスポンスデータはなるべくフラットに。ただし階層構造の方が分かりやすい場合もある
レスポンスのキーはなるべく少ない単語数で表現する
複数の単語を連結する場合、連結方法はAPI全体を通して統一する(JSONではキャメルケース)
日時の形式はRFC3339にする

取得項目を選べるようにする

fields パラメータで項目を指定させる。省略された場合は全項目、 全部だと多すぎるなら最も利用頻度が高いと思われる組み合わせを返す。

項目名を直接指定させる以外に、量の異なるセットをあらかじめ用意して名前で選ばせる方法もある。 Amazon の Product Advertising API では「レスポンスグループ」と呼ばれている。

エンベロープを作らない

メタ情報は HTTP のレスポンスヘッダで表現できる。 そうすればボディは実データだけになって無駄がなくなるし、 ヘッダの書き方を全 API で共通にすればクライアント側の処理を抽象化しやすくなる

階層構造

Google の JSON Style Guide も「なるべくフラットに、ただし階層構造の方が分かりやすい場合もある」 という含みのある書き方をしている。

階層にすべき典型は、送信者(sender)と受信者(receiver)のように同じ構造を持つ場合。 クライアントが両方を同じデータとして処理できるようになり、 毎回 sender receiver の接頭辞を付けるより JSON 自体も小さくなる。

トップレベルは必ずオブジェクト

トップレベルが配列の JSON は JSON インジェクションのリスクがある。 script 要素で他サービスの JSON を読み込ませて、中身を不正に入手する手口。

<script src="https://api.example.com/v1/users/me" type="application/javascript"></script>

仕組みはこう。

  • ルートの {} は JavaScript ではブロックとして解釈されるので、 中に JavaScript のコードがあることが期待される。結果、script 要素で読み込むと構文エラーになる
  • 配列はそれ単体で正しい JavaScript になってしまうので、問題なく読み込まれる

リクエストヘッダに認証情報を入れないと取得できない API ならこの問題は関係ないが、 常にオブジェクトを返す癖をつけておく方が安全

ページングの情報

続きがあるかどうかを hasNext のような名前で結果に含める。 「次があるか」だけでなく、次ページの URI や必要なパラメータを返すパターンもある。

キー名の付け方

なるべく少ない単語数で表現する。 意味を正確に表そうとすると userRegistrationDateTime のように長くなりがちだが、長すぎる。

  • /users というユーザー情報の API なら、先頭の user はなくてよい
  • 時刻には updatedAt のように at を付ける例が多いので、registeredAt にするとさらに短くなる
  • ユーザーが登録した日時はデータが生成された日時とほぼ同じなので、createdAt でも表せる

連結方法(userId / user_id / user-id)については議論が残るが、 JSON ではキャメルケースが良いとされる。ただしスネークケースの API も現実にはたくさんある

値の形式

対象方針
性別フィールド名を gender にしたなら、値は "male" "female" の文字列にする
日時広く一般に公開する API では RFC 33391995-12-17T03:24:00Z / 1995-12-17T03:24:00+09:00。UTC なら Z 表記も可
大きな整数JavaScript は数値をすべて IEEE 754 の64ビット浮動小数として扱うので誤差が出る

Twitter の API は、id に加えて同じ値を文字列として入れた id_str を返すことで対処している。

{
  "id": 266031293949698048,
  "id_str": "266031293949698048"
}

ステータスコード

ステータスコード意味
100番台情報
200番台成功
200OK
201Created
202Accepted
204No Content
300番台リダイレクト
301Moved Permanently(メソッドの変更を許可)
302Found(メソッドの変更を許可)
304Not Modified
307Temporary Redirect
400番台クライアントエラー
400Bad Request
401Unauthorized
403Forbidden
404Not Found
405Method Not Allowed
408Request Timeout
409Conflict
429Too Many Requests
500番台サーバーエラー
500Internal Server Error
502Bad Gateway
503Service Unavailable

200番台はリクエストが成功したときにだけ返す。

エラーなのにボディにエラー情報を入れて 200 を返す実装があるが、これは誤り。 理由は、汎用の HTTP クライアントライブラリの多くがステータスコードで成否を判断しているから。 200 を返すと汎用のエラー分岐が使えなくなり、クライアント側の手間が増える。

ぴったり当てはまるコードがない場合は、200 400 500 のように 00 で終わるコードを使う。

用途ごとの使い分け。

コード使う場面
201ユーザー登録、ToDo の追加、画像アップロードなど、サーバー側に新しいものができたとき
202リクエストした処理が非同期で行われ、受け付けたが完了していないとき
200 / 201PUT・PATCH は 200 と操作したデータを返す。POST は 201
204DELETE

リダイレクト系。

  • リダイレクト先の新しい URI は Location レスポンスヘッダに入る
  • 301 は恒久的な移動、302 は一時的な移動
  • 304 は前回取得から更新されていないことを表す。ボディは空になる。 返るのは、クライアントがキャッシュを持っていてその情報を送ってきたときだけ
  • 302 と 301 は POST から GET へのメソッド変更を許可するが、307 と 308 は許可しない

クライアントエラー系。

  • 400 は「その他」。 他の 400 番台では表せないエラーに使う
  • 401 は認証(Authentication)、403 は認可(Authorization)のエラー
  • 409 はリソースの競合。 ID などのユニークキーを指定して登録する API で、既に同じ ID がある場合など
  • 404 は、ユーザーが存在しないのか URI 自体が間違っているのか切り分けられない。 利用者の開発効率のために、詳しい情報を添えて返す

エラー情報

エラーはヘッダに入れたくなるが、現実に公開されている API はほとんどがボディに格納している。 クライアント側から見た処理のしやすさが理由と思われるので、ボディに入れる方針で問題ない。

Twitter はエラーを配列で返す。

{
  "errors": [
    {
      "message":"Bad Authentication data",
      "code":215
    }
  ]
}

エラー時にボディが HTML になってしまう API がある。 500・503・404 で多く、存在しないエンドポイントへのアクセスや、 コードのバグで処理が止まった場合に起きる。これは避ける。

メンテナンスで止める必要がある場合は 503 で停止中であることを伝える

503 Service Temporarily Unavailable
Retry-After: Mon, 2 Dec 2013 03:00:00 GMT

エラーは具体的かつ正確に返すのが原則だが、セキュリティ上あえて曖昧にしたい場合もある。 詳しい情報はデバッグには役立つので、開発環境では正確に、本番環境では曖昧に返すことで両立できる。

エラー情報はボディに格納する
エラー時のボディがHTMLにならないようにする
メンテナンス時は503とRetry-Afterを返す
本番では、セキュリティ上あえて情報を曖昧にする判断もある

キャッシュ

ヘッダ使いどころ
Expires「毎日何時」のように更新のタイミングが決まっているもの
Cache-Control定期更新ではないが更新頻度がある程度限られているもの。リアルタイム性がそれほど重要でない場合や、負荷のためにアクセス頻度を下げてほしい場合
  • 両方を指定した場合は、より新しい仕様である Cache-Control が優先される
  • Expires に過去の日付や不正な値を入れたときの挙動はブラウザによって違うので、 Cache-Control だけを使う方がよい

条件付きリクエストの流れ。

  1. クライアントが最終更新日時を If-Modified-Since で送る
  2. サーバーは送られてきた情報と現在の情報を比較する
  3. 変更がなければ 304、変更があれば 200 と変更後の内容を返す。 このとき新しい最終更新日時やエンティティタグも送る

更新とキャッシュについて一番理解しているのはサーバー側なので、 「どれくらいキャッシュすべきか」の情報はきちんと返す。 期間を返せない場合でも Last-Modified などの更新情報は発信しておくと、 クライアントが無駄なアクセスを減らせる。

Vary は、キャッシュを一意に特定するのに URI 以外のどのリクエストヘッダを使うかを指定する。 URI が同じでもリクエストヘッダによって内容が変わるケースがあるため必要になる。

キャッシュなしの場合はCache-Control: no-cache
If-Modified-Since: Tue, 01 Jul 2014 00:00:00 GMT
Railsの場合はif stale?(last_modified: @post.updated_at)

Content-Type

メディアタイプが正しく設定されていないとセキュリティ上の問題になる。

JSON を誤って text/html で配信した場合、XMLHttpRequest 経由なら問題なく取得も解析もできてしまう。 だがその URI を直接叩くと、ブラウザは Content-Type を見て HTML として表示する。 結果、内部に埋め込まれた JavaScript が実行される。

{"data":"<script>alert('xss');</script>"}
Content-Type: application/json; charset=utf-8
XSSが可能になるので、メディアタイプは正しく指定する
X-Content-Type-Options: nosniff で Content Sniffing を止める

逆に言えば、JSON にユーザーが送った情報(名前など)を入れている場合でも、 Content-Typeapplication/json が指定されていれば、 多くのブラウザでは直接そのデータにアクセスしても問題は起きない

Content Sniffing は、データの内容からデータ形式を推定するブラウザの機能。

CORS

特定のサイトからのみ許可: Access-Control-Allow-Origin: https://www.example.com
すべて許可: Access-Control-Allow-Origin: *
クライアントは Origin: https://www.example.com をリクエストヘッダーに入れる
事前チェックは OPTIONS メソッドで行われる
Cookie や Authentication ヘッダで認証している場合は Access-Control-Allow-Credentials: true
XMLHttpRequest でのみ使用可能な API の場合は X-Requested-With: XMLHttpRequest

注意点が2つ。

  • CORS で X-Requested-With を付けると、プリフライトリクエストが必要になる
  • 逆に言えば、X-Requested-With を要求することで XMLHttpRequest などヘッダを追加できる方法からのみアクセス可能な API にできる
OPTIONS /v1/users/12345 HTTP/1.1
Host: api.example.com
Accept: application/json
Origin: http://www.example.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: X-RequestId

HTTP/1.1 200 OK
    Date: Mon, 01 Dec 2008 01:15:39 GMT
    Access-Control-Allow-Origin: http://www.example.com
    Access-Control-Allow-Methods: GET, OPTIONS
    Access-Control-Allow-Headers: X-RequestId
    Access-Control-Max-Age: 864000
    Content-Length: 0
    Content-Type: text/plain

※ 例の X-RequestId のように独自ヘッダを新しく作るなら、X- は付けない (独自ヘッダに X- を付けない)。

JSON のエスケープ

JSON の文字列中で許可されていない文字は、ダブルクォート・バックスラッシュ・コントロール文字だけで、 それ以外のすべての Unicode 文字は格納できる。

だからこそ明示的にエスケープする。

  • <> をエスケープすれば SCRIPT 要素を完全に無効化できる。 JSON ではそのまま書くことも許可されているが、<> に変換する
  • \ " '\ " ' の16進エスケープにしておくと、 誤認識でトラブルになる可能性が下がる
{"data":"<script>alert('xss');<\/script>"\}

XSRF 対策

最も一般的なのは XSRF トークン

正規のフォームに、そのサイトが発行したワンタイムトークン (少なくともセッションごとにユニークなトークン)を埋め込んでおき、 それがないアクセスは拒否する

関連

2件のページ