【Web API: The Good Parts】最低限押さえておきたいAPI設計のTips

非常に長期に渡り読みかけで放置されていた名著 [Web API: The Good Parts](https://amzn.asia/d/8ymEyDg) をやっと読破したので、実務ですぐに取り入れやすいと感じた部分についてかいつまんでまとめてみたいと思います。

内容としては以下のチェックが入った章を対象とします。

- [ ] 1章 Web APIとは何か
- [x] 2章 エンドポイントの設計とリクエストの形式
- [x] 3章 レスポンスデータの設計
- [x] 4章 HTTPの仕様を最大限利用する
- [ ] 5章 設計変更をしやすいWeb APIを作る
- [ ] 6章 堅牢なWeb APIを作る

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

### 短く入力しやすいURI

以下のURIでは、ホスト名とパスの両方に`api`が含まれており、意味が重複しています。また、類似した概念である`service`という単語も含まれています。

```text:重複した意味の単語が含まれるURIの例
http://api.example.com/service/api/search
```

以下のようなURIでも、何らかの検索を行うためのAPIであることは判別できるため、同じことを表すならば短くシンプルな方が良い設計といえます。

```text:シンプルなURIの例
http://api.example.com/search
```

### 人間が読んで理解できるURI

一般的でない略語を使用して無理にURIを短くしようとすると使用者にわかりづらくなってしまいます。

以下の例だと、`e`が何を表ているのかさっぱりわかりませんし、`crEmp`はかろうじて`create employee`か？と推測できそうですが実際のところは設計した人にしかわかりません。

```text:省略形を用いたURIの例
http://api.example.com/e/crEmp
```

省略形を使用するのは、国際規格等で標準化されているもののようにAPIを利用する人の共通認識となっているものだけにしましょう。

### 大文字小文字が混在していないURI

大文字小文字の混在はAPIをわかりづらくさせるため、小文字で統一します。

```text
http://api.example.com/TREND
http://api.example.com/Users/AddFriends
```

### 改造しやすい(Hackableな)URI

URIの構造はドキュメントに明記されるべきですが、ドキュメントを熟読せずともどういった設計になっているのかが想像しやすいURIだと使う側にとっては嬉しいはずです。

```text:HackableなURIの例
http://api.example.com/products/123
```

上記の例では、`123`の部分が製品IDを表しており、この部分を変更すれば別の製品情報が取得できると推測できます。

### サーバ側のアーキテクチャが反映されていないURI

サーバ側のアーキテクチャは利用者にとってどうでもいい上にURIの複雑性が増すというだけでなく、悪意を持った利用者に情報を与えてしまうので避けましょう。

```text:アーキテクチャが反映されたURIの例
http://api.example.com/phpscripts/db_query.php?id=123
```

### ルールが統一されたURI

以下のURIを見てみると、IDの指定方法や単数形／複数形が統一感なく使用されています。

```text:統一感がないURIの例
http://api.example.com/users?id=100
http://api.example.com/user/100/post
```

クライアント実装時に間違いなく混乱するので、以下のようにルールを統一したURIにしましょう。

```text:統一感があるURIの例
http://api.example.com/users/100
http://api.example.com/users/100/posts
```

## HTTPメソッドとエンドポイント

URIを操作するリソースそのものとすれば、[HTTPメソッド](https://developer.mozilla.org/ja/docs/Web/HTTP/Methods)はリソースの操作方法といえます。やりたい操作と整合したHTTPメソッドを使用しましょう。

| メソッド名 | 説明 |
|:-|:-|
| GET  | リソースの取得  |
| POST  | リソースの新規登録  |
| PUT  | リソースの更新  |
| DELETE  | リソースの削除  |
| PATCH  | リソースの一部変更  |

:::note warn
メソッドを全てGETにするようなことはしないようにしましょう。Refererやアクセスログからパラメータが参照可能になるなど、セキュリティ上の問題も発生する恐れがあります。
:::

GETやDELETEは言葉の意味からもイメージが付きやすいかと思いますが、POST、PUT、PATCHはどのように使い分ければいいでしょうか。

以下のようなカラムを持つ`users`テーブルが存在すると仮定して、1つずつ見ていきたいと思います。

- id
- name
- email

### POST

POSTは新しいリソースを作成するリクエストとして使用します。`users`にレコードを追加するリクエストの例としては以下のようになります。

```text:HTTPリクエストメッセージの例
POST /users HTTP/1.1
Content-Type: application/json

{
  "name": "nyanko",
  "email": "cat@example.co.jp"
}
```

:::note
説明に不要なヘッダーは割愛しています。また読みやすさ重視でエンコードはしていません。
:::

レスポンスの例としては以下のようになります。新規にデータが登録され、新しいURIへアクセス可能になります。

```text:HTTPレスポンスメッセージの例
HTTP/1.1 201 Created
Location: http://api.example.com/users/123
```

### PUT

PUTはリソースを新たに作成するか、指定したリソースをリクエストのペイロードで置き換えます。

[MDNの説明](https://developer.mozilla.org/ja/docs/Web/HTTP/Methods/POST)では、POSTとPUTの主な違いは、PUTが[べき等](https://ja.wikipedia.org/wiki/%E5%86%AA%E7%AD%89)であることと説明されています。

> PUT と POST との違いは、 PUT がべき等であることです。一度呼び出しても複数回呼び出しても成功すれば同じ効果になる（副作用がない）のに対して、同じ POST に成功すると、複数回の注文を行うような、追加の効果が出ます。

:::note
複数回同じPUTリクエストを送信した場合に結果が変わるような設計になっている場合、使用すべきHTTPメソッドはPUTではないかもしれないので見直してみましょう。
:::

`users`のIDが123であるデータに対して、`name`と`email`を変更するリクエストの例です。

```text:HTTPリクエストメッセージの例
PUT /users/123 HTTP/1.1
Content-Type: application/json

{
  "name": "nyanko_san",
  "email": "cat-san@example.co.jp"
}
```

レスポンスについては[MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Methods/PUT#%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9)に倣い、新たにリソースが作成された場合と、正常に更新が行われた場合でレスポンスの内容を分ける場合それぞれの例を挙げてみます。

```text:HTTPレスポンスメッセージの例: 新たにリソースが作成された場合
HTTP/1.1 201 Created
Location: http://api.example.com/users/123
```

```text:HTTPレスポンスメッセージの例: 既存のリソースが更新された場合
HTTP/1.1 200 OK
Location: http://api.example.com/users/123
```

:::note
特別な理由がない限り、新しいリソースを作成する場合はPOST、既存データを修正する場合はPUTを利用するようにしましょう。
:::

### PATCH

PUTメソッドがリソース全体を置き換えるのに対し、PATCHメソッドはリソースの一部を更新する場合に使用します。

以下にリクエストの例を挙げます。

```text:HTTPリクエストメッセージの例
PATCH /users/123 HTTP/1.1
Content-Type: application/json

{
  "name": "nyannyan"
}
```

PATCHメソッドの場合は新たにリソースが作成されることは想定されていないため、200または204をレスポンスとして使用するのが一般的のようです。

```text:HTTPレスポンスメッセージの例
HTTP/1.1 200 OK
```

## APIのエンドポイント設計

### 複数形の名詞を使用する

データベースのテーブル名に複数形を用いるのが適切であるといわれるのと同様に、`users`や`friends`は「集合」を表しているので、URIに用いる単語は複数形の方が適切であるとされています。

:::note
実際には単数形を使用しているサービスもあります。いくつか調べてみた感じでは、英語圏のサービスは複数形を使用してることが多い印象でした。

| サービス | 単数形／複数形 |
|:-:|:-:|
| GitHub  | 複数形  |
| Notion  | 複数形  |
| Twitter | 複数形 |
| YouTube | 複数形 |
| LINE | 単数形 |
| 楽天 | 単数形 |
| Qiita | 複数形 |
:::

### スペースやエンコードを必要とする文字を使わない

以下のようなエンドポイントがあったとしてもなんのことだかわからないため、エンコードが発生してしまう文字や空白は含まないようにします。

```text:エンコードが発生してしまうURIの例
http://api.example.com/%E3%81%AD%E3%81%93s/123
```

### 単語を繋げる必要がある場合はハイフンを利用する

以下に挙げるように2つ以上の単語をつなぐ方法はいくつかありますが、どれを使用すべきでしょうか。

- スパイナルケース(ケバブケース)
- キャメルケース
- アッパーキャメルケース(パスカルケース)
- スネークケース

[Web API: The Good Parts](https://amzn.asia/d/8ymEyDg)においては、特にポリシーがない場合はスパイナルケースを使用すべきと述べられています。理由としては、ホスト名と同じルールでURI全体を統一するには最も適しているからです。

:::note
そもそも単語はなるべく繋げないようにするのがベストです。いくつも単語を繋げるケースがあったら、それは本来パスで切り分けるべきかもしれないので見直してみましょう。
:::

## 検索とクエリパラメータの設計

### クエリパラメータとパスの使い分け

API側でのパラメータの受け取り方をクエリパラメータにしようか、パスパラメータにしようか迷うことはあるかと思います。そういった際には、以下の観点で検討してみます。

- 一意なリソースを表すのに必要な情報かどうか
- 省略可能かどうか

先ほどから何度か例に挙げているURIで説明すると、`users`からIDが123のユーザーの情報を取得する場合には、IDは一意なリソースを表すために使用されているので、パスパラメータで受け取るようにします。

```text:パスパラメータを使用する例
http://api.example.com/users/123
```

一方で以下のようにソート順を指定するような場合には、送信するパラメータは一意なリソースを表すためには不要(=省略可能)であるため、クエリパラメータを使用します。

一般的な設計では、省略した場合にはデフォルトの値が使用されるケースが多いです。

```text:クエリパラメータを使用する例
http://api.example.com/users/123/posts?sort=created&direction=desc
```

## レスポンスデータの設計

### データの内部構造の考え方

レスポンスデータでまず考えるべきなのは、APIへのリクエスト回数を少なくなるように設計するということです。

そのためには、エンドポイントをどういった用途で使用してもらうかをよく検討する必要があります。以下のような、IDで指定したユーザーの投稿を取得するエンドポイントを例として考えてみます。

```text:ユーザーの投稿を取得するURIの例
http://api.example.com/users/123/posts
```

上記のエンドポイントのレスポンスが以下のように投稿のIDを単に配列で返す形だったらどうでしょうか。

```json:投稿のIDのみを返すレスポンスの例
{
  "posts": [12345, 23456, 34567, ...]
}
```

このエンドポイントのユースケースとしては、画面に投稿の一覧を表示する場合などが考えられます。投稿のID一覧を単に画面に表示させるといったケースは考えにくいはずです。

そのため、投稿のIDを取得した後、別のエンドポイントにリクエストを行ってタイトルや投稿日、本文などを追加で取得する必要があります。

要するにリクエストの回数が増えることになります。

こういったケースではレスポンスに投稿内容の詳細情報も含めてあげるのがいいでしょう。

```json:必要な情報をまとめて返すレスポンスの例
{
  "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のデータ構造でいうと、以下のようにデータを統一された構造でくるむことを指します。

```json:エンベロープを使用したレスポンスの例
{
  "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`のコードを使用するようにします。

:::note info
詳細なステータスコードについては後述します。
:::

### エラーの詳細をクライアントに返す

エラーの詳細情報は大きく分けて2つの方法で返すことができます。HTTPのレスポンスヘッダに入れる方法、レスポンスボディで返す方法です。

いくつかの大手サービスのAPIドキュメントを読んでみましたが、ボディに詳細情報を入れて返すものが多かったです。

クライアントでの取り扱いの容易さとデバッグのしやすさもあるので、ボディにエラー情報を入れて返すのがいいでしょう。

以下はNotionが公開しているAPIのエラーレスポンス例です。

```json: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."
}
```

:::note info
`object_not_found`はNotionが独自に定義しているエラーの詳細コードです。
:::

<!--
### エラー詳細情報には何を入れるべきか

エラーの詳細情報には最低限以下の情報を含めるようにします。

- エラーの詳細コード
- エラーの詳細情報へのリンク
-->

## HTTPの仕様を最大限利用する

### ステータスコードを正しく使う

「ステータスコードでエラーを表現する」の部分でも触れた通り、ステータスコードにはさらに具体的な意味を持ったコードが定義されています。

主にAPIで利用する可能性があるステータスコードを以下に示します。

| ステータスコード | 名前 | 説明 | 関連メソッド |
|:-|:-|:-|:-|
| 200  | OK  | リクエストは成功した  | すべて  |
| 201  | Created  | リクエストが成功し、新しいリソースが作成された  | POST<br />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<br />POST<br />DELETE  |
| 410  | Gone  | リソースは以前は存在したが消滅した  | すべて  |
| 413  | Request Entity Too Large  | リクエストボディが大きすぎる  | すべて  |
| 414  | Request-URI Too Long  | リクエストされたURIが長すぎる  | すべて  |
| 415  | Unsupported Media Type  | 指定されたメディアタイプはサポートされていない  | PUT<br />POST  |
| 429  | Too Many Requests  | 一定の期間内のリクエスト回数が多すぎる  | すべて  |
| 500  | Internal Server Error  | サーバ側でエラーが発生した  | すべて  |
| 503  | Service Unavailable  | サーバは一時的に停止している  | すべて  |

APIによってはすべてのリクエストに200を返し、エラーが発生したかどうかという情報はレスポンスボディで記述しているケースもあります。

しかし、こうした設計は以下のような問題点があります。

- APIの利用者には200番台はリクエスト成功、400 / 500番台はリクエスト失敗といった共通認識があるので混乱を招く
- 汎用的なHTTPのクライアントライブラリは基本的にステータスコードを見て大まかな振る舞いを決めるため、適切でないステータスコードを返した場合に問題を引き起こす可能性がある etc.

最低限、上に示したステータスコードは押さえておき、適切なステータスコードを返却するようにしましょう。

### メディアタイプの指定

メディアタイプはデータの形式のことで、リクエストやレスポンスのデータがテキストなのかJSONなのかといったことを示すために利用します。

レスポンスではContent-Typeというヘッダを利用してメディアタイプを指定します。

```text:Content-Typeの例
Content-Type: text/html
Content-Type: application/json; charset=utf-8
```

1つ目の`text/html`はレスポンスのデータがHTMLであることを意味しています。２つ目の`application/json`はJSONを意味します。

メディアタイプはMIME(Multipurpose Internet Mail Extensions)タイプとも呼ばれており、元々は電子メールの添付ファイルの形式を指定するために開発されましたが、現在ではHTTPレスポンスやAPIのデータ交換に広く使用されています。

メディアタイプは以下の形式で構成されています。

```text:メディアタイプの形式
type/subtype[; parameter=value]
```

タイプはそのデータ形式が大まかにテキストなのか、画像なのか、音声なのかといったカテゴリを表し、サブタイプが具体的なデータ形式を表します。パラメータは省略可能なオプションの部分で、文字コードやマルチパートの区切り文字の指定などに使用します。

タイプは[IANA(Internet Assigned Numbers Authority)](https://www.iana.org/assignments/media-types/media-types.xhtml)によって定義されており、以下の10個が用意されています。

| タイプ | 意味 | 例 |
|:-|:-|:-|
| text  | 人間が読んで理解できるテキストデータ  | text/plain<br />text/html |
| image  | 画像データ  | image/jpeg<br />image/png  |
| audio  | 音声データ  | audio/mpeg  |
| video  | 動画データ  | video/mp4  |
| application  | 他のタイプに当てはまらないデータ  | application/javascript<br />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`と設定している場合、悪意のあるスクリプトがレスポンスに含まれていると、そのスクリプトが実行されるリスクがあります。

```json:スクリプトが実行されてしまう可能性があるJSONレスポンスの例
{"data": "<script>alert('xss');</script>"}
```

特に、IEではContent-Typeヘッダーを無視して、レスポンスの内容を自動的に判別する機能(Content Sniffing)が存在します。

そのため、正しいメディアタイプを設定することに加え、`X-Content-Type-Options: nosniff`ヘッダーを設定することでブラウザがContent Sniffingを行わないようにすることが推奨されています。

### リクエストデータとメディアタイプ

レスポンスヘッダと同様、リクエスト時にもメディアタイプは利用されます。
主に使われるヘッダは以下の２つです。

- Content-Type
- Accept

Content-Typeヘッダはリクエストのボディがどのような形式であるかをサーバに知らせるために使用します。例えば、JSONデータを送信する場合は、`Content-Type: application/json` を設定します。

Acceptヘッダはクライアントがどの形式のレスポンスを受け入れることができるかをサーバーに知らせるために使用します。例えば、クライアントがHTMLレスポンスを希望する場合は、`Accept: text/html`を設定します。

Acceptヘッダーでは、複数のメディアタイプを指定することができます。それぞれのメディアタイプに優先度を示すQuality Value(品質値)を設定することも可能です。品質値は0から1の範囲で指定し、値が大きいほど優先度が高くなります。

```text:Acceptヘッダで優先度を指定する例
Accept: application/json, text/plain; q=0.8, text/html; q=0.5
```

qを指定しなかった場合の品質値は1とみなされ、最優先になります。そのため、上記の例では左から記載している順に優先度が高いことになります。

しかし多くの場合、複数のメディアタイプを指定する必要性はあまりなく、受け取りたいメディアタイプのどれかをAcceptヘッダに指定すれば十分と思われます。

## まとめ

長くなってしまいましたが、今回記事にした内容をひとことでまとめると「独自仕様は避け、なるべくHTTPの仕様に準ずる」ということになるかと思います。

HTTPに限った話ではありませんが、プロトコルとなっている(特に明文化された)仕様に従うことで、利用者に誤解なく使ってもらえる確率は高まるので自分も意識していきたいと思います。

## 参考

### 書籍

- [Web API: The Good Parts](https://amzn.asia/d/ilfvNuk)
- [Webを支える技術 ―― HTTP，URI，HTML，そしてREST](https://amzn.asia/d/5igQxIT)
- [体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践](https://amzn.asia/d/8ltzzZN)

### URL

- [MDN HTTP](https://developer.mozilla.org/ja/docs/Web/HTTP)
- [Twitter API](https://developer.twitter.com/en/docs/api-reference-index)
- [GitHub API](https://docs.github.com/ja/rest/pulls/reviews?apiVersion=2022-11-28)
- [Notion API](https://developers.notion.com/reference/intro)
- [YouTube Data API ](https://developers.google.com/youtube/v3/docs?hl=ja)
- [LINE Messaging API](https://developers.line.biz/ja/reference/messaging-api/)
- [Rakuten Webservice](https://webservice.rakuten.co.jp/documentation)
- [Qiita API](https://qiita.com/api/v2/docs)