🔨

Web API 設計入門

2021/06/14に公開

普段 Web アプリケーションの開発を行っているにも関わらず、そういえば Web API のお作法をちゃんと勉強したことなかったなあ...と思いたち
少し古いんですが、以下の本を読んで、要約(+ セキュリティ部分加筆)してみました。

https://www.oreilly.co.jp/books/9784873116860/

気になる方は是非本書を読んでみてください。歴史的背景なども載っているので、より理解が深まるかと思います。

※ Web API の設計および実装には厳密な規約があまりなく、この記事の内容は慣習的な要素を多分に含んでいます

対象

  • WebAPI を作ったことがない人
  • 作ったことがあるけど雰囲気で作っちゃってる人

HTTP などの詳細な話はしません

Web API リクエストのプラクティス

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

短く入力しやすい

  • 短い方が覚えやすく入力間違いも減る
    • × https://api.example.com/service/api/search => 重複もあるし無駄な情報もある
    • https://api.example.com/serach => これで伝わる

単語にこだわる

人間が読んで理解できるようにする

  • API を利用する開発者が読んで理解できる形にすることで開発のミスを減らすことができる
    • https://api.example.com/sv/u のような形にすると svu が何を表すのかわからない
  • 無闇に略語を使わない
    • 誰もが英語のネイティブではないため、極力略さず使う
    • ただし、国コードを表す jp や 羽田空港を表す HND など企画化されているものは別

スペルミスに気をつける

  • 普通に当たり前のことだが、公開すると後から変更しづらいので、メソッド名や変数名を書く時以上に細心の注意を払う
  • 間違えた場合、ドキュメントのミスなのか API エンドポイントそのもののスペルミスなのかわからないので、利用者からの問い合わせが増える

大文字小文字を混在させない

  • 基本はドメイン名部分に合わせて小文字にする
  • クライアントが大文字小文字を混在したリクエストを送って来たときに取りうる挙動として以下があるが、昨今は 404 を返すケースが多くなっている
    • 混在していない時と同じ結果を返す
    • 小文字だけの正しい URI にリダイレクトする
    • 404 Not Found を返す
  • ちなみに web ページの場合は、複数の URI で同じ結果を返すと google のページランクが下がったりするのでリダイレクトするのが懸命

改造しやすくする(Hackable)

  • ある URI を見れば、他のリソースへのアクセスパターンが類推できるように設計する
    • 例えば以下の URI は末尾の数字(リソース ID)を変えれば他のリソースにアクセスすることが直感的にわかる
      • https://api.example.com/v1/items/12345
  • エンドポイントの URI を Hackable にする必要がないという意見もあるが、これは単にわかりにくいケースを容認するということではない

サーバー側のアーキテクチャは隠蔽する

  • サーバー側のソフトウェアや言語の詳細は API 利用者にとっては必要のない情報なので URI には含めない
    • 例えば phpcgi などのサーバー側都合の語句を入れ込まない
  • 内部アーキテクチャの情報をさらけ出すのは攻撃者にヒントを与えているようなもの

ルールを統一する

  • 同じサービスの中でルールが統一されていないと誤解を招きやすく、利用する側のミスも増える
    • 単数形と複数形が意図なく混ざっていたり、リソースの指定がクエリストリングになっていたりパスに入れる形になっていたりとバラバラにするのを避ける

動詞ではなく名詞を使用する

  • HTTP の URI はそもそもリソースを表すものなので名詞を使用し、動詞は極力使用しない
  • 以下は get_users が冗長で、HTTP メソッドが GET であればリソースの取得というのはわかる
    • × https://api.example.com/v1/get_users/12345
    • https://api.example.com/v1/users/12345
  • 動詞は HTTP メソッドで表す

リソースの集合を表すには複数形を使用する

  • 以下はユーザーの集合の中から id=12345 のユーザーを表すため、複数形 users を使う
    • × https://api.example.com/v1/user/12345
    • https://api.example.com/v1/users/12345
  • 単複同型や複数形が単数形と大きく乖離する単語もあるので注意が必要
    • mouse -> micemedia -> medium など

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

  • URIにおいて使用できない文字を使うと、パーセントエンコーディングと呼ばれるエンコード処理が行われる
  • パーセントエンコーディングされるとぱっと見でエンドポイントがどのようなものかわかりづらくなる
  • ASCII の範囲内にも %&+ などパーセントエンコーディングが含まれる文字があるので注意が必要

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

  • web ページの URI はケバブケースのものが多いが、決まったルールや決定的な理由はないのである程度好みで決めてしまって良い
  • 以下はケバブケースを使う理由
    • Google がケバブケースを推奨していて、SEO 的に良い(多分)
      • Google はハイフンは単語の繋ぎとみなし、アンスコは一続きの単語としてみなす
    • URI のドメイン部分はハイフンは許可されているがアンスコは許可されておらず、大文字小文字の区別もない
  • 実際は単語を繋げること自体をできるだけ避けるべき
    • 例えば popular_users とするのではなく users/popular とするなど

HTTP メソッドについて

HTTP の仕様として定義されているのは以下の6種類

メソッド名 説明
GET リソースの取得
POST リソースの新規登録
PUT 既存リソースの更新
DELETE リソースの削除
PATCH リソースの一部変更
HEAD リソースのメタ情報の取得

GET メソッド

  • リソースの取得を表すメソッド
  • ブラウザの A 要素を使ったリンクは全て GET として扱われる
  • GET アクセスではリソースの取得のみを行い、リソースの変更を行うべきではない
    • GET アクセスした結果、例えば閲覧履歴を更新するなど、対象のリソース以外のデータを更新することはその限りではない

POST メソッド

  • 新しいリソースを登録することを表すメソッド
  • 単に情報の更新を表すと思われがちだが、それは正確ではない
    • HTML4.0の Form では method 属性に GET と POST しか指定できないため、ブラウザからの HTTP リクエストではリソースの登録だけでなく、更新や削除も POST メソッドを使うというのが一般的な認識として普及してしまった
    • Web API の場合は、アクセスの意味をより明確にするために、POST は登録のみと定義してしまった方が良い

PUT メソッド

  • 指定されたリソースの更新を行うことを表すメソッド
    • HTTP の定義的には、存在しなければ作成、存在していれば更新となるが、Web API としては登録は POST、更新は PUT と使い分けるのが一般的
  • PUT は送信するデータでリソースを完全に上書きするというもので、データの一部だけを更新する場合は PATCH を使用する

DELETE メソッド

  • リソースの削除を行うメソッド

PATCH メソッド

  • リソースの一部を変更することを表すメソッド

GET と POST 以外のメソッドが使えない時の考慮

  • HTML Form やクライアントライブラリによっては利用できるメソッドが限られることがある
  • その場合、以下のようにメタ情報として HTTP メソッドを指定することができる
    1. X-HTTP-Method-Override リクエストヘッダで HTTP メソッドを指定する
      • メタデータをクエリパラメーターに含まずに管理できるので、送信データを分類するという観点では good
    2. _method パラメーターで HTTP メソッドを指定する
      • ブラウザの Form に隠しパラメータとして入れることで、ブラウザから API を利用可能にすることができる
      • ただし、 application/x-www-form-urlencoded 以外の形式データを送信する場合は使用できない
  • 様々なクライアント環境を考慮するとどちらかはサポートしておきたい

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

  • データを取得する API の場合、データ量の増加に比例してレスポンスのデータサイズも大きくなっていく
  • パフォーマンスを保つためにはページングやフィルタを用いて、レスポンスデータのサイズが大きくなりすぎないように注意する

ページネーション(取得数と取得位置のクエリパラメーター)

以下のような手段を場合によって使い分ける

  • limit & offset を指定できるようにする
    • limit=50&offset=100
    • クライアントがデータの幅を自由に設定できるので柔軟性が高い
      • 例えば120アイテム目から100アイテムのような指定も可能
  • page & per_page を指定できるようにする
    • per_page=50&page=3
    • キャッシュの効率をあげやすい

ただし、以下のように問題点もあるので設計時は注意が必要

  • バックエンドのクエリ性能劣化
    • DB エンジンによっては相対位置でのデータ取得に対応していなかったり、対応していたとしてもデータ量の増加に伴って速度が落ちていく
  • 相対位置がずれる
    • 更新頻度が高いデータの場合、1ページ目を取得してから2ページ目を取得するまでの間にデータに変更が発生し、ページ内のデータにズレが生じる
    • この問題を解決するためには、絶対位置でデータを指定できるようにする
      • ある ID からの幅を指定したり、時刻を利用したりする方法がある

フィルタリング(絞り込みのクエリパラメーター)

  • 名前やタグなど、何らかの情報を指定して、それに合致するものだけを抽出する
    • http://api.linkedin.com/v1/people-search/first-name=tanaka
  • 部分一致、完全一致、全文検索、and/or 検索、などを考慮する
  • query の略として q というパラメーターが使われることも多い

Web API レスポンスのプラクティス

レスポンスデータフォーマット

  • 基本は JSON に対応し、必要があれば XML にも対応する
    • 今のデファクトスタンダードは完全に JSON なので、 JSON に対応しておけばほとんどの場合問題にならない

データフォーマットの指定方法

  • クエリパラメータを使う
    • https://api.example.com/v1/users?format=xml
    • わかりやすく、もっともよく使われている
  • 拡張子を使う
    • https://api.example.com/v1/users.json
    • 今ではあまり使われていない
  • リクエストヘッダでメディアタイプを指定する
    • Accept: application/json

レスポンスデータの内部構造

  • API のアクセス回数がなるべく少なくなるように設計する

レスポンスデータのサイズを意識する

  • レスポンスのサイズはなるべく小さい方がいいが、情報が不足しすぎると API の呼び出し回数が増える
    • 例えばリソースの ID の一覧を返す API と、 ID を元にリソースの詳細情報を返す API しかないと、一覧で情報を表示したいときに表示したいリソース分だけ API を呼び出さなければいけなくなる
    • そういう API は Chatty API(おしゃべりな API)と呼ばれ、好まれない
    • なるべく少ない呼び出しで一回のアクションに必要な情報を取得できるよう、利用者が使いやすいように設計する
  • レスポンスサイズが大きすぎるのも良くない
    • 単純に通信パケット数が多いと、ソケットにデータを流しきるのに時間がかかるので、呼び出し回数を減らすために全ての情報を一気に返せばいいという訳ではない
  • API アクセスのユースケースを洗い出して、それに基づいてレスポンスを設計する
    • ユースケースが決めきれなくても、クエリパラメータでレスポンスの対象を指定できるようにするなどの柔軟性を持たせることで、大体の場合は対処できる

エンベロープはなるべく使わない

  • 全ての API で同じデータ構造を返すために、本来のレスポンスをさらにラップしたメタデータ部分をエンベロープと呼ぶ
    • 下記の header 部分のようなもの

{
  "header": {
    "status": "success",
    "errorCode": 0
  },
  "response": {
    ...対象のデータ...
  }
}
  • Web API は基本的に HTTP を前提としているので、メタデータとして HTTP のステータスコードや HTTP ヘッダを利用するべき
  • 変に HTTP のレールから外れるとクライアント側で API 呼び出しの実装の抽象化部分が使いまわせなくなったりするという弊害も起きる

なるべくフラットな構造にする

  • 階層化すると多くの場合、サイズが大きくなるので不必要にネストさせない
  • 階層化した方がわかりやすい、あるいは冗長性を排除できる場合のみネストさせる

各データのフォーマット

各データの名前の注意点

  • 多くの API で同じ意味に利用されている一般的な単語を用いる
  • なるべく少ない単語数で表現する
  • 複数の単語を連結する場合、その連結方法は API 全体を通して統一する
  • 変な省略形は極力使用しない
    • 利用クライアントが限定されているかつ、データ転送量がボトルネックになるような場合は省略形を利用して文字数を減らすというのはテクニックとしてはあり
  • 単数系/複数形に気を付ける

性別を表す時は gender と sex を使い分ける

  • 一般的に gender は「社会的・文化的性別」を表し、sex は「生物学的性別」を表す
  • 医療系などのシステムで生物学的な性別を表す場合は sex を使い、SNS や EC 系のシステムであれば gender を使うと良い
  • gender を使う場合、将来的にとりうる値が増えていくことが予想されるので、マジックナンバーより malefemale のように文字列を値として定義した方が良い

日付のフォーマット

  • 基本的にはインターネット標準となっている RFC3399 を使用する
    • 2000-12-31T23:59:59+09:00
  • タイムゾーンは日本限定なら +09:00 を、そうでなければ +00:00 を利用する
  • クライアントが限られている場合は、Unix タイムスタンプ(エポック秒)を使うのもあり
    • Unix タイムスタンプはデータ形式としては単なる数値で、比較や保持が容易かつサイズも小さいという利点がある

大きな整数を扱う場合は文字列として扱う

  • 桁数が大きすぎると値を数値として返すと、クライアントの言語やランタイムの bit 数によってはオーバーフローして誤差が出てしまう可能性がある
  • 登録頻度が高いリソースの ID など、巨大な数を扱う場合は数値ではなく文字列として返す
  • 実際に Twitter の API レスポンスは以下のように id とそれを文字列化したものを返してくるものがある
{
  "id": 26603129394698048,
  "id_str": "26603129394698048"
  ...
}

エラーの設計

HTTP ステータスコードを正しく返す

エラーの詳細をは HTTP レスポンスヘッダ or レスポンスボディで返す

  • レスポンスヘッダに入れる場合、独自に定義したヘッダにエラーの詳細を格納して返す
  • ヘッダとボディのどちらを利用してもいいが、現実に公開されている API はほとんどボディで詳細を表している
  • エラーは配列で返す形にしておくと、同時に複数のエラーが発生した際に全てのエラー情報を返すことができるのでクライアントに優しい設計になる
  • 返す詳細情報の内容としては、エラーコードとエラーメッセージは最低限返す

エラーの際に HTML が返ることを防ぐ

  • 500 や 404 の際に web サーバーやアプリケーションフレームワークがデフォルトで HTML を返してしまう場合があるが、意図せず HTML を返すとクライアントがパースエラーを起こす恐れがある
    • クライアントからすると json や XML が返ってくることを期待しているはず
    • フォーマットを指定してきている場合は尚更
  • 一般公開している API では、web サーバーなどの設定もきちんとチェックし、適切なフォーマットでレスポンスが返るよう注意する

セキュリティ

通信は HTTPS を使う

  • 暗号化されていない通信はパケットスニッフィングでデータを盗み見ることができ、セッションハイジャックなどの対象になる
  • HTTPS を利用すればリクエストライン、リクエストヘッダ、リクエストボディが暗号化される(レスポンスも同様に暗号化される)
  • とはいえ、OpenSSL 自体に脆弱性があったり、証明書を偽造することで盗聴される危険性があることは留意しておく

ブラウザから叩かれる場合はスクリプトの埋め込みに対処する

  • XSS や XSRF、JSON ハイジャックといった脆弱性に対する対策を行う

リクエストの再送信への対処

  • 例えばブラウザのボタンの二重クリックやクライアントアプリケーションのリトライなどによって同じリクエストが複数回なされることは起きうる
  • そのほか悪意のあるユーザーがエンドポイントを解析してリクエストを偽装し、不正に処理をさせようとするかもしれない
  • そういった際に、新しいリクエストがすでに処理されたものと同一かどうかを峻別できるようにして、冪等性を担保する必要がある

大量アクセスへの対処

  • いわゆる DoS 攻撃などで大量アクセスを受ける場合があるが、放置するとサーバーへの負荷が高まってパフォーマンスが劣化したり、最悪の場合はダウンしてしまう
  • レートリミットの機構を入れて、一定時間内に決められた閾値を超えてアクセスしてくるクライアントのアクセスを制限する
    • クライアントの判別はログインしていればユーザーを利用したり、そうでなければ IP アドレスが使えたりする
    • 閾値と単位(一日なのか1時間なのかとか)はユーザーがその API を使うコンテキストやユースケースによって検討する必要がある
    • レートリミットの実装は、クラウドインフラが提供しているものもあるが、自前で実装する場合は KVS などを使ってアクセスを保存したりなどが考えられる

機密性の高い情報は URL に含めない

  • センシティブデータは URL に埋め込むべきではない
    • センシティブデータはクレデンシャル、セッションキー、アクセストークンなど機密性の高い情報を指す
  • URL はブラウザキャッシュやサーバーログ、リファラーログなど積極的にキャッシュされ、サードパーティの分析用に送信される恐れがある
  • その代わりに POST や PUT メソッドのリクエストヘッダーやリクエストボディを利用して送信するべき

クライアントに機密データをキャッシュさせない

  • web ブラウザは UX の観点から可能な限りデータをキャッシングしようとするので、Cache-Control headers を利用してリソースのポリシーを明確にすべし

認証、認可を実装する

  • そもそもこの API を叩けるか、特定のリソースにアクセスできるクライアントなのかをチェックする
  • Authorization ヘッダを利用したり、ボディにクレデンシャルやトークンを埋め込んだりなど

常に web のトレンドを追いかける

  • 脆弱性とその対処はイタチごっこなので、常に最新のセキュリティ動向をチェックし、アップデートを行う

Discussion