- 大多数向けの未知の外部開発者に向けたAPI
- 誰にでも合うフリーサイズの服にちなんで
one-size-fits-all API
とも言う(らしい)
- 自社サービスのスマートフォンクライアント向けのAPIなど、APIを利用する開発者が限られている
- APIとしての汎用性よりも、クライアント側でのUXを考える
1スクリーン1APIコール、1セーブ1APIコール
覚えやすく、どんな機能をもつURIなのかひと目でわかる。
主にLSUDsに向いているポリシー。
- 短く入力しやすい
- 人間が読んで理解できる
- 大文字小文字が混在していない
- 改造しやすい(Hackable)
- サーバー側のアーキテクチャが反映されていない
- ルールが統一されている
- 長いURLは意味が重複している可能性があるので、見直す
- 英語を用いる
- 規格や体系化されたもの以外で、省略系を用いない
- 間違った時制、スペルミスをしない
- 基本的には全て小文字を用いる
TIPS: GitHub, Tumblr, Foursquareは、混在させると404を返す
- URIを修正して別のURIにしやすくする
- 言語や、アーキテクチャがわかると脆弱性になり得るのでやめる
- クライアントの実装時に混乱を招かない
Name | Description |
---|---|
GET | リソースの取得 |
POST | リソースの新規登録 |
PUT | 既存リソースの更新 |
DELETE | リソースの削除 |
PATCH | リソースの一部変更 |
HEAD | リソースのメタ情報の取得 |
- ブラウザのA要素を使ったリンクはすべてGETとして扱われる
- GETメソッドに対して、サーバー側の情報の変更処理を書くのはご法度
- 新規情報を登録するために利用するのが本来の目的
- 指定したURIの配下にデータを登録する
TIPS: HTML4.0のForm
ではmethod属性にGETとPOSTしか指定できなくなったので、削除や更新もPOSTでやるのが普及してしまった
- 更新したいリソースのURIそのものを指定して、その内容を書き換える
- 送信したデータでもともとのリソースを書き換える
- URIで指定したリソースを削除する
- 巨大なリソースを丸ごとPUTで書き換えるのが非効率なときなどに、一部書き換えを行う
HTMLのForm
など、GETとPOSTのみのサポートしかない場合に、POSTを利用しつつ、メタ情報として「本当は使いたいメソッド」を指定する方法。APIを公開する場合には、この対応をした方が良い。
_method
を使う方法もあるが、リクエストデータ中にメタ情報が付与されてしまう。X-HTTP-Method-Override
ヘッダを利用すれば、サーバ側のフレームワークやミドルウェアが自動的に吸収してくれる場合が多い。
SNSのWebAPIを想定してみる。
:id
はユーザーIDを表すプレースホルダー。
目的 | エンドポイント | メソッド |
---|---|---|
ユーザーの一覧取得 | http://api.example.com/v1/users |
GET |
ユーザーの新規登録 | http://api.example.com/v1/users |
POST |
特定ユーザーの情報の取得 | http://api.example.com/v1/users/:id |
GET |
ユーザーの情報の更新 | http://api.example.com/v1/users/:id |
PUT/PATCH |
ユーザーの情報の削除 | http://api.example.com/v1/users/:id |
DELETE |
ユーザーの友達一覧取得 | http://api.example.com/v1/users/:id/friends |
GET |
友達の追加 | http://api.example.com/v1/users/:id/friends |
POST |
友達の削除 | http://api.example.com/v1/users/:id/friends/:id |
DELETE |
近況の編集 | http://api.example.com/v1/updates/:id |
PUT |
近況の削除 | http://api.example.com/v1/updates/:id |
DELETE |
近況の投稿 | http://api.example.com/v1/updates |
POST |
特定ユーザーの近況の取得 | http://api.example.com/v1/users/:id/updates |
GET |
友達の近況一覧の取得 | http://api.example.com/v1/users/:id/friends/updates |
GET |
- 複数形の名詞を利用する
- 利用する単語に気をつける
- スペースやエンコードを必要とする文字は使わない
- 単語をつなげる必要がある場合はハイフンを利用する
- 「集合」を表すため複数形にする
- 末尾に
s
をつけない変則的な複数形になる名詞には注意する - そもそもリソースを扱っているので動詞は入れない
- エンドポイントがどのようなものなのかわからなくなるため
- パーセントエンコーディングなど愚の骨頂
- ある程度は好みで良いがポリシーがないならハイフンにする
- そもそも極力単語をつなぎ合わせるのを避けること
取得数 | 取得位置 |
---|---|
par_page | page |
limit | offset |
count | cursor |
が、一般的な組み合わせ。
limit/offsetの方が自由度が高い。 1ページ50アイテムの3ページめ(101アイテムめから)を取得したい場合、
- per_page=50&page=3
- limit=50&offset=100
pageは1から、offsetは0から数え始める。
- パフォーマンスが悪くなる
- 更新頻度が高いデータでは不整合が起きる
MySQLなどのRDBでは、「先頭から何件めか」を調べるために、先頭から数を数える処理が発生するので、データの件数が増えてくるとパフォーマンスが低下する。
最初の〇〇件を取得してから、次の〇〇件を取得する間にデータの更新が入ってしまった場合に、ズレが生じる。
- データのIDや時刻を記録し、「このIDよりも前」「この時刻より古い」といった指定を行う
TIPS: Twitterのmax_id
はこれで、指定したID以前のものを取得するようにしている。
query
の略- 検索するフィールドがほぼ1つに決まる場合に用いられることが多い
- 全文検索(ユーザー情報の中で文書情報が含まれるフィールドすべてが対象)が直感的であるときもある
q
とフィールド名を組み合わせて検索可能にするケースもある
- 「検索」という意味を明示的に表現する意図としてあり
- 一意なリソースを表すのに必要な情報かどうか
- 省略可能かどうか
- ユーザーIDなど、それを指定することで参照したい情報が一意に決まるものはパスに入れる
- アクセストークンなど、利用者の認可が目的であるものはリソースとは無関係のため、クエリパラメータに入れる
- 省略すればデフォルトの値が利用される
- ユーザーがサービスを利用するときに、ユーザー登録機能をもつ別サービス(SNSなど)に登録している情報を利用して良いという許可を与えること
- ユーザーがOAuthでのアクセスに成功すると、SNS側からアクセストークンが発行される
英語 | 日本語訳 |
---|---|
Authorization | 権限付与, 権限 |
Grant | 許可証, 承認 |
Credential | 権威の証明証, 資格 |
リソースへのアクセスの承認フローは4つ定められている。
GrantType | Purpose |
---|---|
Authorization Code | Railsなどサーバサイドで多くの処理を行うウェブアプリケーション向け |
Implicit | スマートフォンアプリやJavascriptを用いたクライアントサイドで処理の多くを行うアプリケーション向け |
Resource Owner Password Credentials | サーバーサイド(サイトB)を利用しないアプリケーション向け(直接ユーザーからパスワードを受け取ってサーバAからアクセストークンを受け取る) |
Client Credentials | ユーザー単位での認可を行わないアプリケーション向け(社内のAPIサーバー等信頼できるクライアントからOAuthするとき) |
- FacebookやTwitterが提供しているようなのは、Authorization CodeとImplicit
- もし自分で作るAPIでOAuthを提供するのであれば、GrantTypeの実装が必要
TIPS: O'REILLYは↑といっているが、スマートフォンアプリはResource Owner Password Credentialsらしい。 http://qiita.com/awakia/items/66975de18ba25f18a961
/oauth2/token
あたりが妥当。
- OAuth2.0を使っているのが明確
- RFC 6749のサンプルとも類似性がある(らしいが見当たらなかった)
以下のデータをapplication/x-www-form-urlencoded
、UTF-8で送る。
Key | Description |
---|---|
grant_type | password という文字列で、Resource Owner Password Credentialsの認証であることを表す |
username | ログインするユーザー名 |
password | ログインするパスワード |
scope | アクセスのスコープを指定する(省略可能) |
TIPS: scope
は、FacebookならE-mailを取得するemail
や友達一覧を取得するread_friendlists
など。
POST /v1/oauth2/token HTTP/1.1
Host: api.example.com
Authorization: Basic Y2xpzW5021kOmNsaWVudF9zZWNyZXQ
Content-Type: application/x-www-form-urlencoded
grant-type=password&username=mafmoff&password=abc&scope=api
Authorization
ヘッダーは、クライアント認証と呼ばれ、アクセスしようとしているサービスやクライアントアプリケーションが何であるかを特定するための情報。
FacebookやTwitterなどにアプリケーションを登録すると発行されるClient IDとClient Secretを、Basic認証の形式で、Base64変換したものが入っている。
TIPS: Client IDとClient Secretの利用は任意だが、利用することで、アプリケーションごとにAPIのアクセス数を制限したり、ブロックしたりすることができる。
HTTP/1.1 200 OK
// 略
{
"access_token": "b77yz37w7kzy7cffuda6zz91",
"token_type": "bearer",
"expires_in": 2629743,
"refresh_token": "tGzx3JOkF0xF3Qx2TlKWIA"
}
token_type
の"bearer"
はRFC 6750で定義されているOAuth 2.0のトークン形式access_token
の値の送付により、今後Client IDとClient Secretを利用せず、APIにアクセスできるrefresh_token
は、アクセストークンを再発行してもらうためのトークン(返さないことも可能)
- リクエストヘッダに入れる方法
- リクエストボディに入れる方法
- URIにクエリパラメータとして入れる方法
expires_in
は「あと何秒で通行期限を迎えるか」- 有効期限が切れると、サーバは
invalid_token
というエラーをステータスコード401で返す
HTTP/1.1 401 Unauthorized
// 略
{
"error": "invalid_token"
}
- このJSON形式はRFC 6749で、
invalid_token
はRFC 6750で定義されている
以下あたりが妥当。
users/me
users/self
users/:id
とusers/me
では処理が必然的に分岐するので、他人の情報が丸見えになるバグの混入を避けられる。
サービス | エンドポイントの共通部分 |
---|---|
api.twitter.com/1.1 |
|
NetFlix | api-public.netflix.com |
api.linkedin.com/v1 |
- パスに入れるとURIが冗長になる
- ホスト名を分けると、DNSレベルで分割できるので管理しやすい
example.com
というサービスの提供するAPIのホスト名はapi.example.com
が適切。
- APIの返すデータの中に、次に行う行動、取得するデータなどのURIを含める
{
"friends": [
{ "name": "Saeed",
"link": {
"uri": "https://api.example.com/v1/users/12345",
"rel": "user/detail"
}
},
{ "name": "Jack",
"link": {
"uri": "https://api.example.com/v1/users/12346",
"rel": "user/detail"
}
}
]
}
Martin Fowlerさんは言っている。
REST LEVEL | What to do |
---|---|
0 | HTTPを使っている |
1 | リソースの概念の導入 |
2 | HTTPの動詞(GET/POST/PUT/DELETEなど)の導入 |
3 | HATEOASの概念の導入 |
- クライアントは入口となるのエンドポイントだけ知っていれば、その他のURIは知る必要がない
- URIの変更がしやすいので、URIをHackableにする必要がなくなる
- URIの変更に応じたクライアント側の変更が不要になり、保守が容易になる
- エンドポイントがわかりにくくても良いので、アクセスしてほしくないURIを想像しにくくできる
向いているもの。
- スマートフォンアプリなど、変更から配布に時間がかかるもの
- SSDKs、特定のクライアントのみで使われるようなAPI
時期尚早
JSONがデファクトスタンダード。
TIPS: デファクトスタンダードとは「事実上の標準」という意味。
- クエリパラメータに指定する(POSTの場合はフォームデータやボディを含む)
- URIの最後に
.json
や.xml
を付けて指定する Accept
というリクエストヘッダを使う
TIPS: お行儀が良いお作法はリクエストヘッダ利用だが、敷居が高く、クエリパラメータを使う方法が最も普及している。
JSONにそれをラップするJavaScript(padding
)を付け加えたもの。
callback({"id":123, "name":"Saeed"})
TIPS: padding
は「余計なもの」を意味しているので、JavaScriptのコードである必要はない。
- APIのアクセス回数がなるべく減るようにすること
- APIのユースケースをよく考えること
- HTTPのオーバーヘッドが上がり、アプリケーションの速度低下を招くことを避ける
- アプリケーションの特性を踏まえた上で、クライアントが使いやすい構造を検討する
TIPS: 1つの作業を完結するために複数回のアクセスを必要とするAPIの設計は、Chatty(おしゃべりな)APIと呼ばれる。ChattyAPIはネットワークのトラフィックを増加させ、クライアントの処理の手間を増やすめんどうな仕様のAPI。
- クエリパラメータを使って、取得したい情報を指定する
TIPS: ある程度の情報のまとまりを指定する方法に「レスポンスグループ」というセットを使うこともある(AmazonのAPI)
以下のようなメタデータも含む形で、すべてのAPIが同じデータ構造を返すために実際のデータをくるむための構造。
{
"header": {
"status":"success",
"errorCode":0,
},
"response": {
// データ
}
}
- データ形式が共通になるので、クライアントサイドで抽象化しやすい
- JSONPを使うケースに向いている(ステータスコードやレスポンスヘッダにアクセスできないため)
O'REILLY本に言わせると、
- HTTPを利用している場合、HTTPがすでにエンベロープの役割をしている
- HTTPのステータスコードとエンベロープないの
status
の不一致などが起きやすい - メタ情報はHTTPヘッダに書けば良く、レスポンスボディ内の無駄が省ける
「なるべくフラットが良いけれど、階層構造をもった方がわかりやすい場合もあるよね」
by Google JSON Style Guide
その階層化が本当に必要なものかどうかはよく考える。
{
"id":1234,
"name":"Hoge Fuga",
"profile": {
"birthday":1231,
"gender":"male",
"language": ["ja", "en"]
}
}
profile
というカテゴリは、これがなくても見た目的にもコードの処理上でも違いはなく、JSONのデータサイズが大きくなるだけなので、不要。
TIPS: JSONのオブジェクトは順序が考慮されない。
JSONのトップレベルがオブジェクトであれば、
- レスポンスデータが何を示しているのかがわかりやすくなる
- レスポンスデータをオブジェクトに統一することができる
- セキュリティ上のリスクを避けることができる
- 例えば
friends
というキーが付いていれば、データが友人情報であることがひと目でわかる
- クライアント側の受け取り処理が共通化できる
- トップレベルが配列だと、JSONインジェクションの脆弱性に対するリスクが大きくなる
TIPS: JSONインジェクションとは、script要素を利用してJSONファイルを読み込むことによって、ブラウザに他サービスのAPIが提供するJSONファイルを読み込ませ、その内容を不正に入手する方法。
- 20件返すためには最大21件の取得を行ってみて、21件めが取得できれば
"hasNext": true
とする方法
TIPS: ここで、Google*のAPIがやっているように次ページのURIなどを併せて返すとすると、HATEOASっぽくなる。
- 多くのAPIで同じ意味に利用されている一般的な単語を用いる
- なるべく少ない単語数で表現する
- 複数の単語を連結する場合、その連結方法はAPI全体を通して統一する
- 省略形はなるべく使わない
- 単数系/複数形に気をつける
TIPS: Google JSON Style Guideでは、配列の場合は複数形、それ以外は単数系にするというルールがある。
gender
は「社会的・文化的性別」、sex
は「生物学的な性別」gender
の場合は、性別の種類が増えることも考慮してフォーマットは"gender": "male"
のように文字列が良い- 医療系サービスは
sex
、それ以外はgender
が良い
Format | Example |
---|---|
RFC 822 | Sun, 06 Nov 1994 08:49:37 GTM |
RFC 850 | Sunday, 06-Nov-94 08:49:37 GTM |
ANSI Cのasctime()形式 | Sun Nov 6 08:49:37 1994 |
RFC 3339 | 2015-10-12T11:30:22+09:00 |
Unixタイムスタンプ(epoch秒) | 1396821803 |
- LSUDsなAPIに向いている
- W3C-DTFという日付形式の年から秒までをすべて省略せずに含めている
Jan
やFri
など特定の言語に依存した表記を含まず、曜日の表記を含まないので、冗長さが排除されている- HTTPヘッダに用いるHTTP時間においてはタイムゾーン
+00:00
を使うのがわかりやすい
- 1970年1月1日0時0分0秒(UTC:協定世界時)からの経過時間を秒数で表したもの
- データ形式としては単なる数値なので、比較や保持が容易でサイズが小さいので、SSKDsではこちらが使いやすい場合もある
浮動小数などの扱いで、誤差が出てしまうことがある。
- IDなど大きな数字を扱う場合、文字列として返すことで回避できる
エラーは、レスポンスボディで返すだけでなく、まずは適切なステータスコードを返すこと。
ステータスコード | 意味 |
---|---|
100番台 | 情報 |
200番台 | 成功 |
300番台 | リダイレクト |
400番台 | クライアントサイドに起因するエラー |
500番台 | サーバーサイドに起因するエラー |
以下の2つ。
- HTTPのレスポンスヘッダに入れて返す方法
- レスポンスボディで返す方法
独自に定義したヘッダ項目を用意して、情報を入れる。
X-MYNAME-ERROR-CODE: 2013
X-MYNAME-ERROR-MESSAGE: Bad authentication token
X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication
クライアント側からみたときの利便性を考えるとこちらが良い。
{
"error": {
"code": 2013,
"message": "Bad authentication token",
"info": "http://docs.example.com/api/v1/authentication"
}
}
TIPS: 複数のエラーが同時に発生した場合を考慮して、Twitterはエラーが配列で返るようになっている。
- ステータスコードと区別するために、4桁などにする
- 1000番台は汎用エラー、2000番台はユーザー情報のエラーというようにカテゴリごとに分ける
- エラーメッセージは、エンドユーザー向け、開発者向けで分けても良い
- スケジュールされたメンテナンスであれば
Retry-After
というHTTPヘッダを用いて、終了日時を示すと親切
**TIPS: **メンテナンスの時間は長めに見積もっておくこと。
### 敢えてあいまいなエラーを返す
- SNSのブロック機能では、エラー403だとトラブルを招くかもしれないので、404をわざと返す
- ログインAPIなどでは、セキュリティ上どのフィールドがNGなのかがわからない方が良いので、あいまいにする
ステータスコード | 名前 | 説明 |
---|---|---|
200 | OK | リクエストは成功した |
201 | Created | リクエストが成功し、新しいリソースが作られた |
202 | Accepted | リクエストは成功した |
204 | No Content | コンテンツなし |
300 | Multiple Choices | 複数のリソースが存在する |
301 | Moved Permanently | リソースは恒久的に移動した |
302 | Found | リクエストしたリソースは一時的に移動している |
303 | See Other | 他を参照 |
304 | Not Modified | 前回から更新されていない |
307 | Temporary Redirect | リクエストしたリソースは一時的に移動している |
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 | リクエスト回数が多すぎる |
500 | Internal Server Error | サーバー側でエラーが発生した |
503 | Service Unavailable | サーバーが一時的に停止している |
- リクエストメソッドでPOSTが使われたとき
- サーバ上に新しいファイルが作成されたり、データベースのテーブルに項目が追加されたとき
- リクエスト処理が非同期で行われ、処理は受け付けたが完了はしていない状態
- 「処理は開始したけど、終わっていませんよ」という通知
- レスポンスが空のとき
- DELETEメソッドでデータの削除を行ったとき
- PUTやPATCHメソッドでのデータの更新にも使われる(O'REILLYの筆者は勧めていない)
TIPS: 「コンテンツが空」というだけではわかりにくいので、DELETEで削除したデータそのものを返すべきだという主張もある。
主にリダイレクトに関する。
リダイレクトの場合、Location
というレスポンスヘッダにリダイレクト先の新しいURIが含まれる。
HTTP/1.1 302 Not Found
Date: Sat, 23 Nov 2013 12:24:25 GMT
Content-Type: text/plain
Content-Length: 41
Connection: keep-alive
Location: http://example.com
302 Not Found
- ファイルストレージ系のサービスで、指定したキーに対して複数のデータが存在すると返るときがある
- クライアント側でそれまでのデータのキャッシュを行い、キャッシュの情報を返してくれたとき
- レスポンスボディは空になる
- その他の400番台でのエラーでは表すことができないエラー
- 送られてきたパラメータに間違いがあって処理できないとき
- 認証エラー
- 「あなたが誰だかわからないよ」
- 認可エラー
- 「あなたは誰だかはわかったけど、この操作はあなたには許可されていないよ」
- 「アクセスしようとしたデータは存在していないよ」
- エンドポイントは存在しているがメソッドが許可されていない
- たとえば
GET
のみでアクセス可能なAPIにPOST
でアクセスしたとき
- 指定された形式がサポート外のとき
- たとえば
JSON
とXML
しか対応していないのに、YAML
を指定されたとき
- リクエストをクライアントがサーバに送るのに時間がかかりすぎてサーバ側でタイムアウトを起こしたとき
- リソース競合が発生したとき
- すでにそのリソースが存在するとき
- たとえばIDなどのユニークなキーを指定して、データを登録するAPIで、IDが重複しているとき
- 404と似ているが、410はかつて存在したが、今はもうリソースが存在しないとき
- データを削除したという情報を保持する必要がある
TIPS: ユーザーからもわかってしまうので、個人情報保護などの観点から問題を指摘されるかも。