非常に長期に渡り読みかけで放置されていた名著 Web API: The Good Parts をやっと読破したので、実務ですぐに取り入れやすいと感じた部分についてかいつまんでまとめてみたいと思います。
内容としては以下のチェックが入った章を対象とします。
- 1章 Web APIとは何か
- 2章 エンドポイントの設計とリクエストの形式
- 3章 レスポンスデータの設計
- 4章 HTTPの仕様を最大限利用する
- 5章 設計変更をしやすいWeb APIを作る
- 6章 堅牢なWeb APIを作る
エンドポイントの基本的な設計
短く入力しやすいURI
以下のURIでは、ホスト名とパスの両方にapi
が含まれており、意味が重複しています。また、類似した概念であるservice
という単語も含まれています。
http://api.example.com/service/api/search
以下のようなURIでも、何らかの検索を行うためのAPIであることは判別できるため、同じことを表すならば短くシンプルな方が良い設計といえます。
http://api.example.com/search
人間が読んで理解できるURI
一般的でない略語を使用して無理にURIを短くしようとすると使用者にわかりづらくなってしまいます。
以下の例だと、e
が何を表ているのかさっぱりわかりませんし、crEmp
はかろうじてcreate employee
か?と推測できそうですが実際のところは設計した人にしかわかりません。
http://api.example.com/e/crEmp
省略形を使用するのは、国際規格等で標準化されているもののようにAPIを利用する人の共通認識となっているものだけにしましょう。
大文字小文字が混在していないURI
大文字小文字の混在はAPIをわかりづらくさせるため、小文字で統一します。
http://api.example.com/TREND
http://api.example.com/Users/AddFriends
改造しやすい(Hackableな)URI
URIの構造はドキュメントに明記されるべきですが、ドキュメントを熟読せずともどういった設計になっているのかが想像しやすいURIだと使う側にとっては嬉しいはずです。
http://api.example.com/products/123
上記の例では、123
の部分が製品IDを表しており、この部分を変更すれば別の製品情報が取得できると推測できます。
サーバ側のアーキテクチャが反映されていないURI
サーバ側のアーキテクチャは利用者にとってどうでもいい上にURIの複雑性が増すというだけでなく、悪意を持った利用者に情報を与えてしまうので避けましょう。
http://api.example.com/phpscripts/db_query.php?id=123
ルールが統一されたURI
以下のURIを見てみると、IDの指定方法や単数形/複数形が統一感なく使用されています。
http://api.example.com/users?id=100
http://api.example.com/user/100/post
クライアント実装時に間違いなく混乱するので、以下のようにルールを統一したURIにしましょう。
http://api.example.com/users/100
http://api.example.com/users/100/posts
HTTPメソッドとエンドポイント
URIを操作するリソースそのものとすれば、HTTPメソッドはリソースの操作方法といえます。やりたい操作と整合したHTTPメソッドを使用しましょう。
メソッド名 | 説明 |
---|---|
GET | リソースの取得 |
POST | リソースの新規登録 |
PUT | リソースの更新 |
DELETE | リソースの削除 |
PATCH | リソースの一部変更 |
メソッドを全てGETにするようなことはしないようにしましょう。Refererやアクセスログからパラメータが参照可能になるなど、セキュリティ上の問題も発生する恐れがあります。
GETやDELETEは言葉の意味からもイメージが付きやすいかと思いますが、POST、PUT、PATCHはどのように使い分ければいいでしょうか。
以下のようなカラムを持つusers
テーブルが存在すると仮定して、1つずつ見ていきたいと思います。
- id
- name
POST
POSTは新しいリソースを作成するリクエストとして使用します。users
にレコードを追加するリクエストの例としては以下のようになります。
POST /users HTTP/1.1
Content-Type: application/json
{
"name": "nyanko",
"email": "cat@example.co.jp"
}
説明に不要なヘッダーは割愛しています。また読みやすさ重視でエンコードはしていません。
レスポンスの例としては以下のようになります。新規にデータが登録され、新しいURIへアクセス可能になります。
HTTP/1.1 201 Created
Location: http://api.example.com/users/123
PUT
PUTはリソースを新たに作成するか、指定したリソースをリクエストのペイロードで置き換えます。
MDNの説明では、POSTとPUTの主な違いは、PUTがべき等であることと説明されています。
PUT と POST との違いは、 PUT がべき等であることです。一度呼び出しても複数回呼び出しても成功すれば同じ効果になる(副作用がない)のに対して、同じ POST に成功すると、複数回の注文を行うような、追加の効果が出ます。
複数回同じPUTリクエストを送信した場合に結果が変わるような設計になっている場合、使用すべきHTTPメソッドはPUTではないかもしれないので見直してみましょう。
users
のIDが123であるデータに対して、name
とemail
を変更するリクエストの例です。
PUT /users/123 HTTP/1.1
Content-Type: application/json
{
"name": "nyanko_san",
"email": "cat-san@example.co.jp"
}
レスポンスについてはMDNに倣い、新たにリソースが作成された場合と、正常に更新が行われた場合でレスポンスの内容を分ける場合それぞれの例を挙げてみます。
HTTP/1.1 201 Created
Location: http://api.example.com/users/123
HTTP/1.1 200 OK
Location: http://api.example.com/users/123
特別な理由がない限り、新しいリソースを作成する場合はPOST、既存データを修正する場合はPUTを利用するようにしましょう。
PATCH
PUTメソッドがリソース全体を置き換えるのに対し、PATCHメソッドはリソースの一部を更新する場合に使用します。
以下にリクエストの例を挙げます。
PATCH /users/123 HTTP/1.1
Content-Type: application/json
{
"name": "nyannyan"
}
PATCHメソッドの場合は新たにリソースが作成されることは想定されていないため、200または204をレスポンスとして使用するのが一般的のようです。
HTTP/1.1 200 OK
APIのエンドポイント設計
複数形の名詞を使用する
データベースのテーブル名に複数形を用いるのが適切であるといわれるのと同様に、users
やfriends
は「集合」を表しているので、URIに用いる単語は複数形の方が適切であるとされています。
実際には単数形を使用しているサービスもあります。いくつか調べてみた感じでは、英語圏のサービスは複数形を使用してることが多い印象でした。
サービス | 単数形/複数形 |
---|---|
GitHub | 複数形 |
Notion | 複数形 |
複数形 | |
YouTube | 複数形 |
LINE | 単数形 |
楽天 | 単数形 |
Qiita | 複数形 |
スペースやエンコードを必要とする文字を使わない
以下のようなエンドポイントがあったとしてもなんのことだかわからないため、エンコードが発生してしまう文字や空白は含まないようにします。
http://api.example.com/%E3%81%AD%E3%81%93s/123
単語を繋げる必要がある場合はハイフンを利用する
以下に挙げるように2つ以上の単語をつなぐ方法はいくつかありますが、どれを使用すべきでしょうか。
- スパイナルケース(ケバブケース)
- キャメルケース
- アッパーキャメルケース(パスカルケース)
- スネークケース
Web API: The Good Partsにおいては、特にポリシーがない場合はスパイナルケースを使用すべきと述べられています。理由としては、ホスト名と同じルールでURI全体を統一するには最も適しているからです。
そもそも単語はなるべく繋げないようにするのがベストです。いくつも単語を繋げるケースがあったら、それは本来パスで切り分けるべきかもしれないので見直してみましょう。
検索とクエリパラメータの設計
クエリパラメータとパスの使い分け
API側でのパラメータの受け取り方をクエリパラメータにしようか、パスパラメータにしようか迷うことはあるかと思います。そういった際には、以下の観点で検討してみます。
- 一意なリソースを表すのに必要な情報かどうか
- 省略可能かどうか
先ほどから何度か例に挙げているURIで説明すると、users
からIDが123のユーザーの情報を取得する場合には、IDは一意なリソースを表すために使用されているので、パスパラメータで受け取るようにします。
http://api.example.com/users/123
一方で以下のようにソート順を指定するような場合には、送信するパラメータは一意なリソースを表すためには不要(=省略可能)であるため、クエリパラメータを使用します。
一般的な設計では、省略した場合にはデフォルトの値が使用されるケースが多いです。
http://api.example.com/users/123/posts?sort=created&direction=desc
レスポンスデータの設計
データの内部構造の考え方
レスポンスデータでまず考えるべきなのは、APIへのリクエスト回数を少なくなるように設計するということです。
そのためには、エンドポイントをどういった用途で使用してもらうかをよく検討する必要があります。以下のような、IDで指定したユーザーの投稿を取得するエンドポイントを例として考えてみます。
http://api.example.com/users/123/posts
上記のエンドポイントのレスポンスが以下のように投稿のIDを単に配列で返す形だったらどうでしょうか。
{
"posts": [12345, 23456, 34567, ...]
}
このエンドポイントのユースケースとしては、画面に投稿の一覧を表示する場合などが考えられます。投稿のID一覧を単に画面に表示させるといったケースは考えにくいはずです。
そのため、投稿のIDを取得した後、別のエンドポイントにリクエストを行ってタイトルや投稿日、本文などを追加で取得する必要があります。
要するにリクエストの回数が増えることになります。
こういったケースではレスポンスに投稿内容の詳細情報も含めてあげるのがいいでしょう。
{
"posts": [
{
"id": 12345,
"title": "テクノロジーの進化:未来を変えるイノベーション",
"body": "私たちの生活は、テクノロジーの急速な進化によって劇的に変わっています。...",
"created_at": "2024-05-01T01:00:00.000Z",
"updated_at": "2024-05-07T12:00:00.000Z"
},
{
"id": 23456,
"title": "自分らしい働き方を見つける:新しいキャリアの道",
"body": "現代社会では、キャリアの在り方も変化しつつあります。...",
"created_at": "2024-05-02T01:00:00.000Z",
"updated_at": "2024-05-03T12:00:00.000Z"
},
{
"id": 34567,
"title": "サステナビリティの時代:持続可能な未来を築く方法",
"body": "持続可能な未来を目指すために、私たちが個人と社会でできることは何でしょうか。...",
"created_at": "2024-05-04T11:00:00.000Z",
"updated_at": "2024-05-05T02:00:00.000Z"
},
...
]
}
エンベロープは必要か
エンベロープとは「封筒」を意味する単語です。APIのデータ構造でいうと、以下のようにデータを統一された構造でくるむことを指します。
{
"result": {
"status": "success",
"code": 200,
"message": "User data retrieved successfully"
},
"data": {
"user": {
"id": 123,
"name": "nyanko",
"email": "cat@example.co.jp"
}
}
}
レスポンスを上記のような構造にしてしまうのはありがちかと思いますが、HTTPにはresult
に入っているようなメタデータを送信する仕様としてHTTPヘッダが定義されているため、そちらを使用しましょう。
エラーの表現
ステータスコードでエラーを表現する
まず1番初めにやるべきは、正しいステータスコードを返すことです。
以下のように、ステータスコードは何番台かによって大まかなステータスを表しています。
ステータスコード | 意味 |
---|---|
100番台 | 情報 |
200番台 | 成功 |
300番台 | リダイレクト |
400番台 | クエイアントエラー |
500番台 | サーバーエラー |
さらに、例えば200番台であれば201 Created
や202 Accepted
などのより具体的なステータスコードが用意されているので、なるべくこちらを使用し、ぴったり合うものがなければX00
のコードを使用するようにします。
詳細なステータスコードについては後述します。
エラーの詳細をクライアントに返す
エラーの詳細情報は大きく分けて2つの方法で返すことができます。HTTPのレスポンスヘッダに入れる方法、レスポンスボディで返す方法です。
いくつかの大手サービスのAPIドキュメントを読んでみましたが、ボディに詳細情報を入れて返すものが多かったです。
クライアントでの取り扱いの容易さとデバッグのしやすさもあるので、ボディにエラー情報を入れて返すのがいいでしょう。
以下はNotionが公開しているAPIのエラーレスポンス例です。
{
"object": "error",
"status": 404,
"code": "object_not_found",
"message": "Could not find page with ID: 4cc3b486-0b48-4cfe-8ce9-67c47100eb6a. Make sure the relevant pages and databases are shared with your integration."
}
object_not_found
はNotionが独自に定義しているエラーの詳細コードです。
HTTPの仕様を最大限利用する
ステータスコードを正しく使う
「ステータスコードでエラーを表現する」の部分でも触れた通り、ステータスコードにはさらに具体的な意味を持ったコードが定義されています。
主にAPIで利用する可能性があるステータスコードを以下に示します。
ステータスコード | 名前 | 説明 | 関連メソッド |
---|---|---|---|
200 | OK | リクエストは成功した | すべて |
201 | Created | リクエストが成功し、新しいリソースが作成された | POST PUT |
202 | Accepted | リクエストは受け入れられたが、サーバで処理が完了していない | すべて |
204 | No content | リクエストが成功したが、クライアントに返すコンテンツはない | すべて |
300 | Multiple Choice | リクエストに対して複数のレスポンスがある | すべて |
301 | Moved Permanently | リソースは恒久的に移動した | すべて |
302 | Found | リソースは一時的に移動している | POST |
303 | See Other | 他を参照 | POST |
304 | Not Modified | リソースは前回から更新されていない | GET |
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 | リソースが矛盾した | PUT POST DELETE |
410 | Gone | リソースは以前は存在したが消滅した | すべて |
413 | Request Entity Too Large | リクエストボディが大きすぎる | すべて |
414 | Request-URI Too Long | リクエストされたURIが長すぎる | すべて |
415 | Unsupported Media Type | 指定されたメディアタイプはサポートされていない | PUT POST |
429 | Too Many Requests | 一定の期間内のリクエスト回数が多すぎる | すべて |
500 | Internal Server Error | サーバ側でエラーが発生した | すべて |
503 | Service Unavailable | サーバは一時的に停止している | すべて |
APIによってはすべてのリクエストに200を返し、エラーが発生したかどうかという情報はレスポンスボディで記述しているケースもあります。
しかし、こうした設計は以下のような問題点があります。
- APIの利用者には200番台はリクエスト成功、400 / 500番台はリクエスト失敗といった共通認識があるので混乱を招く
- 汎用的なHTTPのクライアントライブラリは基本的にステータスコードを見て大まかな振る舞いを決めるため、適切でないステータスコードを返した場合に問題を引き起こす可能性がある etc.
最低限、上に示したステータスコードは押さえておき、適切なステータスコードを返却するようにしましょう。
メディアタイプの指定
メディアタイプはデータの形式のことで、リクエストやレスポンスのデータがテキストなのかJSONなのかといったことを示すために利用します。
レスポンスではContent-Typeというヘッダを利用してメディアタイプを指定します。
Content-Type: text/html
Content-Type: application/json; charset=utf-8
1つ目のtext/html
はレスポンスのデータがHTMLであることを意味しています。2つ目のapplication/json
はJSONを意味します。
メディアタイプはMIME(Multipurpose Internet Mail Extensions)タイプとも呼ばれており、元々は電子メールの添付ファイルの形式を指定するために開発されましたが、現在ではHTTPレスポンスやAPIのデータ交換に広く使用されています。
メディアタイプは以下の形式で構成されています。
type/subtype[; parameter=value]
タイプはそのデータ形式が大まかにテキストなのか、画像なのか、音声なのかといったカテゴリを表し、サブタイプが具体的なデータ形式を表します。パラメータは省略可能なオプションの部分で、文字コードやマルチパートの区切り文字の指定などに使用します。
タイプはIANA(Internet Assigned Numbers Authority)によって定義されており、以下の10個が用意されています。
タイプ | 意味 | 例 |
---|---|---|
text | 人間が読んで理解できるテキストデータ | text/plain text/html |
image | 画像データ | image/jpeg image/png |
audio | 音声データ | audio/mpeg |
video | 動画データ | video/mp4 |
application | 他のタイプに当てはまらないデータ | application/javascript application/json |
multipart | 複数のデータからなる複合データ | multipart/form-data |
message | 電子メールメッセージ | message/rfc822 |
model | 3Dのオブジェクトやシーンなどのモデルデータ | model/vrml |
example | メディアタイプ使用方法の例示用 | example/foo-bar |
font | フォントデータ | font/woff |
メディアタイプをContent-Typeで指定する必要性
多くのクライアントではContent-Typeヘッダを参照してレスポンスのデータを解釈しています。
APIを利用するクライアントが正しくデータを読み出せるようにするために正確なメディアタイプを指定する必要があります。
例えば、Content-Type: application/javascript
と指定すべきところをContent-Type: text/html
と指定していた場合、クライアントではレスポンスデータをJavaScriptとして解釈を試みてエラーが発生する可能性があります。
メディアタイプとセキュリティ
メディアタイプが正しく設定されていないAPIではクライアントでのトラブルが発生するだけではなく、セキュリティ上の問題も発生する可能性があります。
例えば、APIが以下のようなJSONレスポンスに対してContent-Typeをapplication/json
ではなくtext/html
と設定している場合、悪意のあるスクリプトがレスポンスに含まれていると、そのスクリプトが実行されるリスクがあります。
{"data": "<script>alert('xss');</script>"}
特に、IEではContent-Typeヘッダーを無視して、レスポンスの内容を自動的に判別する機能(Content Sniffing)が存在します。
そのため、正しいメディアタイプを設定することに加え、X-Content-Type-Options: nosniff
ヘッダーを設定することでブラウザがContent Sniffingを行わないようにすることが推奨されています。
リクエストデータとメディアタイプ
レスポンスヘッダと同様、リクエスト時にもメディアタイプは利用されます。
主に使われるヘッダは以下の2つです。
- Content-Type
- Accept
Content-Typeヘッダはリクエストのボディがどのような形式であるかをサーバに知らせるために使用します。例えば、JSONデータを送信する場合は、Content-Type: application/json
を設定します。
Acceptヘッダはクライアントがどの形式のレスポンスを受け入れることができるかをサーバーに知らせるために使用します。例えば、クライアントがHTMLレスポンスを希望する場合は、Accept: text/html
を設定します。
Acceptヘッダーでは、複数のメディアタイプを指定することができます。それぞれのメディアタイプに優先度を示すQuality Value(品質値)を設定することも可能です。品質値は0から1の範囲で指定し、値が大きいほど優先度が高くなります。
Accept: application/json, text/plain; q=0.8, text/html; q=0.5
qを指定しなかった場合の品質値は1とみなされ、最優先になります。そのため、上記の例では左から記載している順に優先度が高いことになります。
しかし多くの場合、複数のメディアタイプを指定する必要性はあまりなく、受け取りたいメディアタイプのどれかをAcceptヘッダに指定すれば十分と思われます。
まとめ
長くなってしまいましたが、今回記事にした内容をひとことでまとめると「独自仕様は避け、なるべくHTTPの仕様に準ずる」ということになるかと思います。
HTTPに限った話ではありませんが、プロトコルとなっている(特に明文化された)仕様に従うことで、利用者に誤解なく使ってもらえる確率は高まるので自分も意識していきたいと思います。
参考
書籍
- Web API: The Good Parts
- Webを支える技術 ―― HTTP,URI,HTML,そしてREST
- 体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践