1章 Web API とは何か
API の公開を検討する
- API エコノミー: Web API を広く公開することで外部サービスとの連携が容易になり、新しい価値が生まれてサービスやビジネスが発展していくこと
- Programable Web: 様々な API の情報を収集・公開する API ディレクトリサービス
- 何を公開すべきか?
- そのサービスができること(コアの価値ある部分)全て
- 公開によって得られるもの
- 他の企業や個人が付加価値を提供してくれる
→ それが有用であれば自分たちで同じ機能を用意することも可能
- 他の企業や個人が付加価値を提供してくれる
API 設計の観点
- 使いやすい
- 使ってほしいから公開するのに使いづらい API にしてしまっては意味が薄れる
- 変更しやすい
- システムは進化していくので、API も変更を余儀なくされる
- ユーザに影響を与えることなく API 変更を行うという観点も含む
- 頑強である
- セキュリティ面が考慮されている
- 恥ずかしくない
- ウェブサービスとは異なり、API を目にするのは主に技術者
- ダサい API を公開していると技術力が疑われる恐れも
API 設計の原則
- 仕様が決まっているものは仕様に従う
- 仕様が決まっていないものはデファクトスタンダードに従う
「REST」
- 本書では REST の考え方を適用する場面が多いものの、厳密な REST の定義にはこだわらない
対象開発者の分類
分類 | 説明 |
---|---|
LSUD (Large Set of Unknown Developers) | Facebook や Twitter など、誰でも使えるパブリックな API のユーザ |
SSKD (Small Set of Known Developers) | 自社サービスのクライアントアプリ向けの API など、利用者が限られた API のユーザ |
LSUD 向けか SSKD 向けかで設計の思想は異なるはず。
2章 エンドポイントの設計とリクエストの形式
- API で提供する機能を決定
- エンドポイント(API にアクセスするための URI)を設計
- URI 設計: 覚えやすく、どんな機能を持つのかひと目で分かるものにする
- 短く入力しやすい(ドメイン名も含めて、無駄に同じ意味を重複させない)
- 人間が読んで理解できる
- むやみに省略形を使わない
- API によく使われる単語を使う
- 大文字小文字を混在させない
- 改造しやすい(Hackable)ようにする
- ある程度、仕様書を見なくとも別の URI の作り方が類推できる
- ex. http://api.example.com/v1/users/1234
- サーバ側のアーキテクチャを反映しない
- アンチパターン: http://api.example.com/cgi-bin/get_user.php
- 複数機能を別のエンドポイントで提供する際はルールを統一する
- 利用する HTTP メソッドも合わせて設計する
- URI 設計: 覚えやすく、どんな機能を持つのかひと目で分かるものにする
エンドポイント設計の注意点
- 「複数形」の「名詞」を使う
- ex.
user
→users
,friend
→friends
- URI はリソースを表すものという考え方から、動詞は極力入れないのが基本
- 慣習的に
search
のように動詞が使われるケースもある
- ex.
- 空白やエンコードを必要とする文字を使わない
- 単語を繋げる必要がある場合はハイフンを使うのがベター
- 厳密なルールはないので最終的にはプロダクトのポリシーや好み次第
- そもそも、繋ぎ合わせずパスを区切ったりクエリパラメータに分離したりする方が見やすくなることが多い
クエリパラメータとパスの使い分け
- 一意なリソースを表すのに必要な情報ならパスに含め、そうでなければクエリパラメータに
- 省略可能な情報ならクエリパラメータに
ログイン
- 仕様としては OAuth が標準的
+--------------+
+------(3)----->| App A |
| +---(4)------| (ex.Twitter) |
| | token +--------------+
| v ^ |
+--------+ | |
| user | token (6) (7) resource
+--------+ | |
| ^ | | v
| | | +--------------+
| | +--(1)----->| App B |
| +----(2)------| (uses App1's |
+------(5)----->| resources) |
token +--------------+
- App A のリソースへのアクセスしてください
- では App A のアクセス許可をもらってください
- App B がリソースへアクセスするのを許可します
- ではこのトークンを App B に渡してください
- このトークンを使って App A からリソースを取得してください
- リソースをください
- どうぞ
HATEOAS と REST LEVEL3 API
Martin Fowler による「REST API の設計レベル」(2010)
LEVEL | 内容 |
---|---|
0 | HTTP を使っている |
1 | リソースの概念の導入 |
2 | HTTP の動詞(GET/POST/PUT/DELETE/…)の導入 |
3 | HATEOAS の概念の導入 |
HATEOAS (Hypermedia As The Engine Of Application State) の思想:
- API が返却するデータの中に、次に行う行動、取得するデータ等の URI をリンクとして埋め込んでおく
- これにより、データを見れば次にどのエンドポイントにアクセスすれば良いかがひと目で分かる
{
"friends": [
{
"name": "Taro",
"link": {
"uri": "https://api.example.com/v1/users/12345",
"rel": "user/detail"
}
},
{
"name": "Jiro",
"link": {
"uri": "https://api.example.com/v1/users/13558",
"rel": "user/detail"
}
}
]
}
REST LEVEL3 のメリット:
- 入口となる最初の API エンドポイントさえ分かっていれば、その先の操作を行うためのエンドポイントは API レンスポンスが教えてくれる
- よって、変更から配布までに時間がかかるクライアントアプリケーションなどにおいては、URI を変更するたびにクライアント側を修正する、あるいは通知するといった手間が不要になる
3章 レスポンスデータの設計
データフォーマット
主流である JSON に対応していれば基本的に問題はない
- Amazon のように XML しかサポートしていないところも
- Yahoo! Japan などは PHPserialize にも対応
複数データフォーマットに対応している場合の指定方法
- クエリパラメータで指定
- URI の最後に拡張子を付与(ex.
http://api.example.com/users.json
) - リクエストヘッダでメディアタイプを指定
format
などのクエリパラメータによる指定が多数派。
JSONP
- 同一生成元ポリシーによる制約を回避するためのテクニック
- セキュリティ上のリスクを抱えるため、最低限必要な箇所だけ利用すべき
データの内部構造の考え方
なるべく必要アクセス回数の少ない API にする
アンチパターン:Chatty API = 1つの作業を完了するために複数回のアクセスが必要な API
- ネットワークトラフィックが多くなる
- ユーザに「面倒くさい仕様」という印象を与えやすい
レスポンスの内容をユーザが選べるようにする
- 毎回全てのデータを返すとデータ量が多くなって望ましくないので、必要な項目だけ選択させる
- ex.
http://api.example.com/users/12345?fields=name,age
- ex.
- 項目が多すぎて指定が大変な場合は、
small
,medium
,large
などのようにサイズや用途別の異なる項目グループを定義してそれを選択できるようにする
エンベロープは不要
エンベロープ:全てのデータを同じ構造でくるむこと
{
"header": {
"status": "success",
"errorCode": 0
},
"response": {
...
}
}
基本的に Web API は HTTP を利用しており、HTTP ヘッダやステータスを使えばこうした構造は不要({"status": "error"}
のような内容を HTTP 200 ステータスで返却するようなケースもあり、好ましくない)。
データはフラットにすべきか
なるべくフラットにするが、階層化した方が分かりやすい場合は階層化、程度のポリシーが良い
ex. 階層化した方が良い場合
{
"sender_id": 1234,
"sender_name": "Taro",
"receiver_id": 2345,
"receiver_name": "Jiro"
}
{
"sender": {
"id": 1234,
"name": "Taro"
},
"receiver": {
"id": 2345,
"name": "Jiro"
}
}
配列とフォーマット
JSON はトップレベルが配列でもオブジェクトでも良いので、レスポンスに複数のオブジェクトを返したい場合は2通りがある
- 配列をそのまま返す
- オブジェクトで包む
[
{
"id": 1234,
"name": "Taro"
},
{
"id": 2345,
"name": "Jiro"
}
]
{
"users": [
{
"id": 1234,
"name": "Taro"
},
{
"id": 2345,
"name": "Jiro"
}
]
}
筆者は2を推奨
- レスポンスデータが何を表すのか分かりやすい
- セキュリティ上のリスクを避けることができる
- トップレベルが配列である JSON は、JSON インジェクション 脆弱性のリスクが大きくなる
- JSON をそのまま JavaScript として読み込んでしまった場合、トップレベルがオブジェクトの場合は構文エラーとなるので安全
配列の件数、続きがあるかを返す
- 検索機能の結果であれば、「全部で何件結果があるか」という情報を一緒に返すと有益なことが多い
- ニュースのタイムラインなどであれば合計件数はさほど重要ではなく、ページネーションのために「続きがあるか?」という情報だけがわかれば良い
{
"pages": [
{
"title": "...",
"url": "..."
},
{
"title": "...",
"url": "..."
},
...
],
"hasNext": "true"
}
各データのフォーマット
データの名前
- 多くの API で同じ意味で利用されている一般的な単語を使う
- なるべく少ない単語数で表現する
- 複数単語の連結方法は API 全体で統一する
- キャメルケース or スネークケース or …
- 世の中の API やスタイルガイドにおいてはキャメルケースが多数派
- スネークケースの方が読みやすいという研究結果もあり難しい問題
- おかしな省略形は極力使わない
- アンチパターン:
timeline
→tl
,location
→lctn
- アンチパターン:
- 単数形・複数形に注意
- 配列を返すなら複数形で
性別の扱い
- 生物学的な区別が必要な場合は
sex
(医療系サービスなど) - 文化的・社会的な区別が必要な場合は
gender
日付のフォーマット
形式 | 例 |
---|---|
W3C-DTF | 2019-03-02T11:39:06+09:00 |
Unix Timestamp | 1551494346 |
レスポンスデータの設計
- API のレスポンス構造が、サービス内部で持つ DB の構造を反映する必要は全くない
- API のユースケースをよく検討し、ユーザが最もシンプルに扱うことができる設計を考える
エラーの表現
- エラー時に「エラーが発生しました」はあまりに不親切
- 適切な HTTP ステータスを使う
- HTTP ステータスはあくまで汎用的なものなので、これだけだと内容が不十分である可能性がある
- HTTP ヘッダ、ないしレスポンスボディにエラー内容の詳細を記述すると良い
- 複数のエラーが同時発生した場合に分けて情報を返せるよう、
{"errors": [...]}
と配列形式にしても良い
{
"error": {
"message": "Bad authentication token",
"code": "4002",
"info": "http://docs.example.com/api/v1/authentication"
}
}
- 開発者向けのデバッグ用メッセージと、クライアントアプリケーションのユーザ向けの表示メッセージを分けておくのも有用な場合がある
- エラー時に HTML ページが返らないようにする
- Web API は Web ページではないので、JSON を期待しているクライアントに html 形式で404ページが返却されたりするとパースエラーを起こすリスク
4章 HTTP の仕様を最大限利用する
ステータスコードを正しく使う
ステータスコード | 意味 |
---|---|
100番台 | 情報 |
200番台 | 成功 |
300番台 | リダイレクト |
400番台 | クライアント起因のエラー |
500番台 | サーバ起因のエラー |
(各ステータスコードの詳細は省略)
キャッシュと HTTP の仕様
ここでいうキャッシュは、「クライアント側が一度サーバから取得した情報を保持しておく」ものを指す。
Expiration Model (期限切れモデル)
あらかじめレスポンスデータの有効期限を決めておき、期限が切れたら再度アクセスしてデータを取得する方式。
- 有効期限を絶対時間で指定
Expires: Fri, 01 Jan 2016 00:00:00 GMT
HTTP 1.1 の仕様によれば、長くとも1年以内にしておくべき。
- 有効期限を現在時刻からの秒数で指定
Cache-Control: max-age=3600
Date: Tue, 01 Jan 2014 00:00:00 GMT
Validation Model (検証モデル)
今持っているキャッシュが有効かどうかをサーバに問い合わせる方式。
- 有効であれば「有効である」という情報だけが返る(
304 Not Modified
) - 期限切れであれば新しいデータが返る
データが更新されたかどうかの検証方式には2種類ある。
- 最終更新日
- エンティティタグ
- データの MD5 ハッシュなどの文字列を使う
データ取得時のレスポンスヘッダ:
Last-Modified: Tue, 01 Jul 2014 00:00:00 GMT
ETag: "ff39b31e284452ee8238fdaca"
更新検証のリクエストヘッダ:
If-Modified-Since: Tue, 01 Jul 2014 00:00:00 GMT
If-None-Matche: "ff39b31e284452ee8238fdaca"
Heuristic Expiration (発見的期限切れ)
サーバ側から明示的な期限の情報が与えられなかった場合に、サーバの更新頻度や状況などを参考に、クライアント側が独自にキャッシュの期限を決める方式。
キャッシュさせたくない場合
刻一刻と変わる情報を取り扱うため、クライアントがキャッシュしてしまうとサービスに悪影響を及ぼしてしまうようなケース。
Cache-Control: no-cache
Vary でキャッシュの単位を指定
- 同じ URI であっても、リクエストヘッダの内容によってレスポンスが変化する場合がある
- ex.
Accept-Language: ja
の場合は日本語でデータを返す - このような場合、キャッシュする際はリクエストヘッダの内容も加味すべき
- ex.
- サーバ側でレスポンスに Vary ヘッダを付与して、「URI が同じでもこのリクエストヘッダが違うとレスポンスが変わるかも」と教えることができる
Vary: Accept-Language,User-Agent,Cookie
メディアタイプの指定
- 全ての API はレスポンスに
Content-Type
ヘッダを付与すべき- ブラウザをはじめ、クライアントの多くがまずこれを見てデータ形式を判定
application/x-msgpack
のようなサブタイプがx-
で始まるものは、IANA に未定義の形式- 自分で定義する場合
- サブタイプに適切な接頭辞を
vnd.
: 広く使われる想定だが特定の企業や団体が管理prs.
: 実験的な実装や公開される予定のない製品x.
: 未登録のもの。vnd.
,prs.
でカバーできるため現在は非推奨(x-
は RFC 6838 で廃止済み)
- JSON や XML など既存のフォーマットを使った新しい形式を定義する場合は、
application/foo+xml
,application/bar+json
のようにする
- サブタイプに適切な接頭辞を
同一生成元ポリシーとクロスオリジンリソース共有
同一生成元ポリシー (Same Origin Policy)
XMLHttpRequest などにおいて、「そのウェブページと同じ生成元」のデータのみ、読み込みを許可するというセキュリティ上のポリシー(ブラウザなどが内蔵)。
悪意あるウェブページからデータを利用させないことを目的とする。
- 「同一生成元である」=「スキーム(http, https など)、ドメイン、ポート番号が同じである」
- XMLHttpRequest:
- すでに読み込んだウェブページから更に http リクエストを発行するための技術
- ex. ブラウザ上で解釈された html の script タグ内からのアクセス
- ブラウザから呼び出される API を構築する際、API だけドメインを
api.example.com
のように別にしてしまうとこの制約でデータが読み込めない - JSONP の利用で回避できるがセキュリティ上の問題も多くできれば使うべきではない
クロスオリジンリソース共有 (Cross Origin Resource Sharing, CORS)
特定の生成元からのアクセスのみを許可する仕組み。
- サーバ側(
http://api.example.com
)で予め、「この生成元からならデータへのアクセスを許可する」というリストを持っておく - クライアントが
Origin: http://www.example.com
のようにアクセス元となる生成元の情報をリクエストヘッダに含める - サーバ側でこのアクセス元をリストと照合
- 許可された生成元でなければ
403 Forbidden
- 許可された生成元であれば
Access-Control-Allow-Origin: http://www.example.com
のようにレスポンスヘッダを付与し、許可されたことを示す
- 許可された生成元でなければ
独自の HTTP ヘッダを定義する
適切な情報を送るための HTTP ヘッダが存在しない場合、X-
の接頭辞をつけて(必須ではない)自分で定義できる
5章 設計変更をしやすい Web API を作る
API をバージョンで管理する
- 基本激に、一度公開した API の設計変更は行うべきでない
- サービスの改善に設計変更が必要な場合は、別のエンドポイント or 別のパラメータによるアクセス先を新しく用意する(= 複数バージョンの API を用意)
http://api.example.com/v3/search
のように URI のパス部分の先頭にバージョンを埋め込むのが一般的
- バージョン番号の管理方法としては セマンティックバージョニング が有名
${major}.${minor}.${patch}
の3つの整数を使う形式- パッチバージョン:API に変更がないバグ修正など
- マイナーバージョン:API に後方互換性のある変更があった場合
- メジャーバージョン:API に後方互換性のない変更があった場合
- メジャーバージョンのみを URI のパスに含めると良い
http://api.example.com/users/1234?version=3
のようにリクエストパラメータにバージョンを含める方法も- リクエストパラメータを省略した場合のデフォルトの共同がクライアントから分かりにくく、エラーの原因になりがち
バージョンを変える際の指針
- メジャーバージョンアップはサーバもクライアントも移行コストが大きく、可能な限り避けるべき
- 項目名の変更程度であれば、マイナーバージョンアップで新旧両方の項目名を返すように修正し、ドキュメント上で
Deprecated
などとラベリングしておきメジャーバージョンアップの際に整理すれば良い
- 項目名の変更程度であれば、マイナーバージョンアップで新旧両方の項目名を返すように修正し、ドキュメント上で
- どういうときにメジャーバージョンアップを行うのか?
- 認証必須化など、セキュリティや権限上のルール変更
- ルールが整理されずに進化し続けてしまった API を整理
- (あくまで一例であり、明確な指針は存在しない)
API の提供を終了する
- 事前の告知を行い、実際に終了するまでに十分な猶予期間(最低6ヶ月程度)を設ける
- Twitter では、実際に古い API が停止するまでに “Blackout Test” が何度か実施された
- 一時的に API を停止してアクセスできないようにするテスト
6章 堅牢な Web API を作る
- 個人情報などを取り扱う場合は HTTPS を
- XSS/XSRF などウェブサイトと共通の脆弱性だけでなく、JSON ハイジャック のような API 固有の脆弱性も
- セキュリティ関係の HTTP ヘッダを適切に利用する
- レートリミットを設け、一部のユーザによる大量アクセスを防ぐ
- ユーザごとのカウンタが必要になって大変
- KVS を利用する方法がよく使われる