LoginSignup
30
42

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

Last updated at Posted at 2024-05-22

非常に長期に渡り読みかけで放置されていた名著 Web API: The Good Parts
やっと読破したので、実務ですぐに取り入れやすいと感じた部分について
かいつまんでまとめてみたいと思います。

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

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

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

短く入力しやすいURI

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

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

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

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

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

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

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

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

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

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

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

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

改造しやすい(Hackableな)URI

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

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

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

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

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

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

ルールが統一されたURI

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

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

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

統一感がある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
  • email

POST

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

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

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

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

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

HTTPレスポンスメッセージの例
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であるデータに対して、nameemailを変更するリクエストの例です。

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

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

レスポンスについてはMDNに倣い、新たにリソースが作成された場合と、
正常に更新が行われた場合でレスポンスの内容を分ける場合
それぞれの例を挙げてみます。

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

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

PATCH

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

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

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

{
  "name": "nyannyan"
}

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

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

APIのエンドポイント設計

複数形の名詞を使用する

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

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

サービス 単数形/複数形
GitHub 複数形
Notion 複数形
Twitter 複数形
YouTube 複数形
LINE 単数形
楽天 単数形
Qiita 複数形

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

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

エンコードが発生してしまうURIの例
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で指定した
ユーザーの投稿を取得するエンドポイントを例として考えてみます。

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

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

投稿の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 Created202 Acceptedなどの
より具体的なステータスコードが用意されているので、なるべくこちらを使用し、
ぴったり合うものがなければX00のコードを使用するようにします。

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

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

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

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

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

以下はNotionが公開している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の例
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と設定している場合、
悪意のあるスクリプトがレスポンスに含まれていると、
そのスクリプトが実行されるリスクがあります。

スクリプトが実行されてしまう可能性があるJSONレスポンスの例
{"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ヘッダで優先度を指定する例
Accept: application/json, text/plain; q=0.8, text/html; q=0.5

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

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

まとめ

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

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

参考

書籍

URL

30
42
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
42