[1] Web API (HTTP による API) の悪い設計例やどう設計するべきかの指針のご紹介です。
[138] 何が良いかは諸説あり宗教的で難しいですが、 悪いものには悪い理由があるので賛否はともかく参考になるはずです。
[2] API にバージョンを設けるべきではありません。 API の拡張は、常に後方互換性を保った形で行うべきです。
[3] 従って URL や HTTPヘッダーなどにバージョン番号を指定させる必要はありません。
[7] やむを得ず非互換変更せざるを得なくなった時は、非互換な機能のみ、 新しいエンドポイントを設けるべきです。はじめから非互換変更するつもりで準備しておくのは無駄ですし、 非互換変更するための言い訳を用意するようなもので、危険です。
[38] バージョンが複数あるということは、同じものを複数メンテナンスし続けなければならないということです。バグやセキュリティーホールのリスクも増えます。場合によっては、混用された時におかしなことにならないかも注意しなければなりません。
[39] 実際それはつらいので、いつか古いバージョンへの対応をやめようとします。しかし、古いバージョンで問題なく使えていたクライアントは、無駄な変更コストが発生しますし、それによって新たにバグが生じることもあります。面倒に思ったり、コスト上の理由などで、それを機に対応を取りやめるクライアントもあるかもしれません。旧バージョンのアクセスが減ったと思って廃止したら、アクセス頻度が低いクライアントが利用できなくなって困る、というのもありそうです。 バージョンを分ければ変更しやすくなるというのはその場しのぎでしかなく、 問題は先送りされているだけです。
[88] 結局、API 提供者にとっても、クライアント開発者にとっても、クライアント利用者にとっても、嬉しいことは何一つありません。
[4] 実用上、 GET
と POST
(と OPTIONS
と HEAD
)
以外の要求メソッドを使うべきではありません。
[6] 意味的にもすべての操作をこの2つで十分明確に表現できます。
[79] PUT
や DELETE
を使うべきだというのは宗教的な主張なので、
普通は POST
で問題ありません。
最近は PATCH
教も人気ですが、
流行に乗ればいいというものでもないでしょう。
[5] 特殊な要求メソッドに対応していないクライアントのための代替手段として
X-HTTP-Method-Override:
ヘッダーに要求メソッドを指定できるようにすることがありますが、
本末転倒です。正しい要求メソッドを使うべきです。
[32] 安全で冪等な操作にしか GET
を使う(使える)べきではありません。
かつては JSONP のためやむを得ず GET
を使うことがありましたが、
今やただのセキュリティーホールです。
[8] 認証には HTTP 基本認証か OAuth 2.0 Bearer
を使うべきです。いわゆるAPIキー認証は Bearer
で表現できます。 (OAuth 2.0 の認可フローを実装しなくても、
Bearer
だけ使えます。)
[31] キャッシュ (サーバーアプリケーションを構成するプロキシ、 またはクライアントが使っているプロキシのもの。) による問題を防ぐためには、 HTTP認証の仕組みに乗るべきです。その意味で query や独自 HTTPヘッダーにAPIキーを指定する方法は、安全ではありません。
[129] Bearer
と実質的に同じものなのに、独自の auth-scheme
名としている Web API もありますが、何のメリットもありません。避けるべきです。
[80] 独自の署名を求める方式も、避けるべきです。歴史的には意味があったかもしれませんが、 今や HTTPS が基本ですから、本当に署名が必要な状況は稀です。 署名はデバッグが難しく、開発者を悩ませてきました。
[22] 例えば検索キーワードなどは URL query で指定するべきです。
[23] path に任意の文字列が指定可能な場合、 クライアント側のパーセント符号化漏れ、 Web API サーバー側の逆プロキシや WAF の設定ミスによるパーセント符号化まわりの不具合などが起こりがちです。
[34] よく path は名詞がよく、動詞は好ましくないと言われます。
[48] ただし整理上の都合で動詞を使った方が綺麗になるなら、無理に避けるものでもありません。
たとえば https://host/document/124345
の編集に関する操作は
https://host/document/124345/edit
への POST
に、
削除に関する操作は
https://host/document/124345/delete
への POST
に実装しても何ら問題ないでしょう。
https://host/document/124345/edit
の GET
が編集ページ、
https://host/document/124345/delete
の GET
が削除確認ページになっていると、ちょうど良い。[66] オプションを引数 (URL query や要求本体の
application/x-www-form-urlencoded
)
ではなくパスの一部に入れたがる人もいるようです。
[67] あまりどちらを使うべきか一概に言えるものでもないのですが、 次のような判断基準はたてられそうです。
[81] API に登場する語 (path の一部分、引数の名前、 列挙型の引数の値、 JSON のオブジェクトに含まれる名前など) は、一貫した命名規則に従うべきです。
[83] 省略形よりも完全形の方がわかりやすくて良いとする人もいます。 しかしやたらと長い方がかえってわかりにくくなったり、面倒くさかったりもします。 省略するべきかどうかは適宜判断すればよく、語の長さよりも一貫性の方が重要でしょう。
[93] 英単語を連ねる場合、 camelCase、 -
、_
、.
、
区切りなしなどいくつかの流儀があります。プログラミング言語の変数名などと同じく、
色々な人が色々な好みを持っていて、どれが優れているというものでもありません。
これもまた、一貫性を持たせることさえ心掛ければ、どの方法でも良さそうです。
[11] 状態符号は適切に選択するべきです。エラーなら 4xx
とするべきです。
4xx
参照。[47] エラーだろうがなんだろうが構わず 200
を返すものは最悪です。
本文 JSON データでエラーを表している場合でも、適切な状態符号を使うべきです。
[128] ライブラリーによっては状態符号で正常かエラーかを判断したり、 開発者に有用なデバッグ用メッセージを生成したりといった機能があります。 これは応答がおおよそどのような性質のものであるかを状態符号という HTTP 全体で共通の方法で伝達できることの恩恵です。 たとえ詳細なエラーを別の方法で伝える必要があるとしても、 それはそれとして、状態符号は正しく設定するべきです。
[94] 一昔前、 XML-RPC や SOAP が若干流行ったことがありました。 しかし今では誰も使っていません。
[95] WebDAV や Atom API・AtomPub が使われたこともありました。 今でも皆無ではありませんが、新規開発なら気にする必要はありません。 既存サービスとの互換性のようなことも、考えるだけ時間の無駄でしょう。
[96] Web API のやり取りを共通化するプロトコルはこの他にも色々と提案されてきましたが、 どれも大きな成功を収められていません。 Webアプリケーションはそれぞれに異なる要件と利用形態を持っていて、 どれだけ共通化しても、どうしても収まりきらない部分が出てきてしまいます。 するとサービスごとに独自対応せざるを得なくなりますが、 それならば初めからそれぞれの特性や利用方法に応じた独自の API とした方が、提供側も利用側も楽だったりするのでしょう。 HTTP を使う、 JSON を使うといった基盤といえる部分を除けば、 Web API のプロトコルは標準化には馴染まなそうです。
[97] Twitter と Facebook は、扱うデータの形態や利用者のアクションだけみれば API を共通仕様に揃えられそうなものですが、 両者の実際の API のエンドポイントやデータ構造は全く異なります。 設計思想や歴史的な経緯の違いももちろんあるのでしょうが、 同じように見えても実際には全然異なる機能だったり、 違った形で利用されていたりして、簡単に共通化できるものでもなさそうですし、 それが各サービスの差別化要素にもなっているのでしょう。
[41] 競合他サービスの API に似せた API を提供しようとして、失敗している例をたまに見かけます。
[42] 完全互換ならクライアント開発者のメリットも多いでしょうが、実際には細かな差異がたくさんあったり、他サービスの仕様変更に追随できなかったりして、似て非なるものになっているのが普通です。すると他サービス用のコードを思ったほど流用できなかったり、境界ケースで正しく扱えず不具合が生じたりしがちです。
[44] 困るのは互換性があると信じて有名競合サービスとそれに似せた弱小サービスに対応したクライアントの開発者です。 有名競合サービスが仕様を変更し、弱小サービスが追随できない場合、 細かな分岐を増やすなり、コードを丸々コピーするなりしてがんばって両方への対応を維持し続けるか、 弱小サービスのサポートを終了するか、 弱小サービスのことを忘れていて仕様変更対応後に不具合が出てから気がつくか。 それならはじめから別のサービスは別のコードで実装していた方が良かったのではないでしょうか。
[43] クライアント開発者の視点からすると、 (対象となるサービス自体のデータモデルが似ていることが前提で) 同じ操作が似たような手順で実行さえできればよく、そのエンドポイント名や引数名、応答のデータ構造などは (完全に一致しないなら) 合わせなくても良いように思えます。
[29] 常に HTTPS を使うべきです。平文のHTTP が好ましい理由は何もありません。 歴史的な理由で未だに HTTP で Web API を提供するサービスも少なくありませんが、 それに倣うべきではありません。
[99] iOSアプリから利用する場合、 ATS により標準で HTTPS のみ有効になっています。アプリに設定を変更させるのは危険であり、避けるべきです。
[9] 出力データ形式は JSON を基本とするべきです。 ほとんどのプログラミング言語やフレームワークで、 JSON は標準で実装されています。
[33] XML は追加ライブラリーが必要で面倒なクライアント環境やデータの扱いが面倒なライブラリー API が多いので、極力避けるべきです。
[50] CBOR や MessagePack のようなバイナリーベースの形式を好む人もいます。 確かにサイズは抑えられ、 (実装次第ですが) JSON より高速に処理できるかもしれませんが、 ほとんどの場合は微々たる差で、より広く普及していてデバッグも容易な JSON の方が適切です。少なくても不特定多数の開発者に公開された API では、 JSON を基本とするべきです。
[12] JSONP は使うべきではありません。ただのセキュリティーホールです。 異なる起源の JavaScript からのアクセスが必要なら、 CORS を使うべきです。
[10] データ形式を選択制にするときは、URL query または拡張子で指定できるようにするべきです。
Accept:
による選択は使うべきではありません。
杜撰なキャッシュの実装は Accept:
を正しく扱えません。
URL で指定できる方がデバッグが容易です。
サーバーのアクセスログに普通 Accept:
は記録されないので、URL に含めた方が便利です。
[49] Webブラウザー向けと Web API で同じ URL
を使い Accept:
によって内容折衝するような実装方法もありますが、
混乱を招くだけで誰も得しないので、避けるべきです。
[36] クライアントが機械的に処理することを想定したエラー応答は、 JSON など (成功時と同じ) 機械可読な形式で提供するべきです。例えば投稿エンドポイントは、 投稿エラーなら、投稿エラーを説明した JSON 応答を返すべきです。
[37] しかしだからといって、無理にどんなエラーでも JSON で返すようがんばる必要はないでしょう。例えばアプリケーションサーバーが過負荷で応答できないときのリバースプロキシのエラーまで敢えて JSON にしなくても構いません (しても構いませんが)。どうせクライアントは状態符号以上の有益な情報をその応答から得られません。 クライアントはどのみち JSON でない時のエラー処理を実装しておく必要がありますから、 実装の手間も変わりません。
[123] JSON の MIME型は application/json
です。
ほとんどすべての場合、これをそのまま使って何の問題もありません。
[124] たまに、 application/service-name+json
のような独自の MIME型を使っていることがあります。 MIME型は独自なのに、
データは普通の JSON だったりします。 MIME型の仕組みとして間違ったことではありませんが、
何のメリットもない、意味のないことです。
[78] MIME型の引数 (application/example; foo=bar
の foo=bar
のような部分) を使いたがる人がいるようです。一見便利に使えそうな場所ですし、
実際意味的に適切な場合もあるのですが、 MIME型の引数の取扱いは相互運用性に欠けるので、
あまりおすすめできません。
[142] Web API の versioning に濫用した事案:
application/vnd.elasticsearch+json
[45] JSON の場合、最上位は (配列やプリミティブ値ではなく) オブジェクトとした方が無難です。何かのリストを返したい時も、 後から他の付加データを増やしたいときに配列だとどうしようもないので、 オブジェクトでくるんでおくと良いでしょう。
[46] 識別子などで桁数の多い数字列を扱う時は、 JSON の数値ではなく文字列で表すように注意するべきです。
[118] JSON の null
、ブール型、数値、文字列の違いは、
プログラミング言語によっては区別されないことや違いが曖昧なことがあります。
従って、これらの違いを厳密に扱うことを要求するべきではありません。
[119] 例えば、クライアントから {"foo": true}
または
{"foo": false}
のような値を受け取りたい場合、
JavaScript の ToBoolean
の挙動に寄せて、
... のようにすると、いろいろなプログラミング言語のライブラリーから無理なく扱えて好ましいと思われます。
(更に Perl の bool context の挙動に寄せて、文字列 0
も偽とみなすのもありかもしれません。)
[51] HAL のような JSON ベースの言語を採用したり、 JSON Schema のようなスキーマに従って定義したりすることを好む人もいます。 そうしたものが有用な場面もあれば、あまり意味のない場面もありますから、 個別の用途に合わせて何を採用するか決めるべきです。「すべて○○を使うべき」 といった主張は眉唾物です。
[13] 日時形式は、Unix time を基本とするべきです。 ISO 8601の日時形式やHTTPの日時形式、その他独自の形式は、 構文解析や生成処理や時間帯処理が面倒でミスを誘発しがちなので、避けるべきです。
[25] クライアントによるページングの指定は、 URL query で行うのが伝統的な方法です。 それで何も問題ありません。
[26] サーバーによる前後のページのリンクなどは、本文 JSON データに含めるべきです。
Link:
ヘッダーによる指定は構文解析や生成処理が面倒でミスを誘発しがちなので、避けるべきです。
本文に含められない時は、 URL だけを値とする独自の HTTPヘッダーを使うのが次善策です。
[76] 「投稿一覧」や「コメント一覧」のような追加や削除が起こり得る対象の一覧を時刻順で取得する API では、ページングにページ番号や先頭からの位置を取得するべきではありません。 最初のページと次のページの取得時点で位置が変化している可能性があるので、 項目の重複や欠落が発生する可能性があるからです。
[77] 「時刻 t より前、後」のような指定形式が良さそうですが、 同時刻に複数の項目が存在するかもしれないケースでは、 取り扱いが難しくなるかもしれません。 マイクロ秒やナノ秒の精度の時刻を使えば、そのようなケースはほとんど排除できそうです。 秒の精度では、重複はかなりの頻度で発生します。
[57] 人間向けサーバーと Web API のサーバーをどのように共存させるかは、 アプリケーションの実装手法やサービスの規模と負荷分散の戦略によって、 いろいろな手法が考えられます。実際、いろいろな方法が用いられています。
[58] 例えば:
[62] 最初は小規模のサービスが大きくなった場合でも、 URL で区別できるなら、 リバースプロキシで接続先アプリケーションサーバーを切り替えられますから、 負荷分散戦略によって URL の設計を過剰に制約する必要はありません。
[15] 機械的に生成されただけのものは、ドキュメントとは言えません。 むしろドキュメントを整備した気になるだけなので有害です。
[27] 例えば JSON Schema から機械的に生成した HTML ファイル群は、ただの JSON Schema の書き下し文であって、 ドキュメントではありません。 もしや有用な情報が含まれているのでは、と読者の期待を煽ってしまうだけなので、 むしろ有害です。
[16] 入出力や処理内容など必要な情報を過不足なく記述するべきです。 エンドポイントごとに一言コメントを並べたようなものは、メモ書きであり、 ドキュメントではありません。
[17] また例示は例示であり、ドキュメントの本編ではありません。 例示をいくつ並べても、エンドポイントの説明とはいえません。
[131] 主要なプログラミング言語での利用例を示すこと自体は構いませんが、 HTTP でどのような要求を送信すると、どのような応答を受信することになるか、 という説明を怠ってはいけません。 クライアントがどのようなプログラミング言語でどのようなライブラリーを用いていようと、 HTTP という共通のプロトコルで等しく API にアクセスできることが、 Web API の特徴であり、可能性です。
[28] API によってアクセスされる対象であるオブジェクトの説明をがんばっているのに、 肝心の Web API の部分、つまり HTTP でどのような要求が必要で、 どのような応答が送られるのかの説明が雑なことがよくあります。 サービスのオブジェクトモデルの説明は、もちろん重要な情報ですが、 それだけでは Web API のドキュメントになりません。
[63] 一読して理解できないレイアウトは避けるべきです。 API ドキュメントは、 デザイナーやエンジニアの自己表現の場ではありません。
[102] 表示上の固定部分 (position: fixed
など) はできるだけ避けるべきです。
ノートPCなどの狭い画面や、広い画面でも開発中のアプリケーションの窓やエディターの窓などと同時表示している場合など、
狭い表示領域しか与えられないことがあります。
[132] 同じ理由で、本文の左右に目次や例示などで広い幅を占有する領域を配置するのは避けるべきです。
[100] 無駄にページ分割された構成は、一見整理されて使いやすく思えますが、 どんな機能があるか全体像をよく理解していない API 利用者にとっては望む機能がどこにあるか探しにくかったりします。 少ないページにまとめられていた方が、どんな機能が提供されているのか眺めやすいです。 例えばエンドポイントごとに別ページに分かれていたりすると、 極めて使いづらいドキュメントになります。
[101] 同じような理由で、見出しをクリックすると詳細説明が展開されるような、 一々細かく操作しないと全体を読めないドキュメントも使いづらいです。
[141] 一つの Web頁に内容がまとまっていれば、 Webブラウザーの標準のページ内検索機能で求めるものを見つけることができて、 便利なのです。
[103] 本サイトへのリンクがどこかにあるべきです。 (ドキュメントトップページへのリンクがあっても本サイトへのリンクがない (かわかりにくい) ことは意外とよくあります。)
[139] WebブラウザーのWeb頁内検索を呼び出そうと Ctrl + F を押すと、キャンセルして独自の検索機能を表示するドキュメントがあります。 ほとんどすべての場合、 Webブラウザー標準の検索機能の方が優秀なので、邪魔なだけです。 (Webブラウザー事業者で勤務するトップクラスの技術者が時間をかけて作ったものと、 どちらが便利か、少し考えればわかりますよね。)
[140] ドキュメント (複数の Web頁) の横断検索は、あれば便利なので、 別にフォームを用意して貰えればそれで良い。 Webブラウザーの標準機能を潰す必要はどこにもない。
[110] 他の開発者との情報交換や、 ソースコードのコメントなどの形でドキュメントの一部を参照したいことがよくあります。 従って、
[107] ドキュメントは Webサイトで簡単に参照できるようにしておくべきです。
[130] API の URL の path 部分しか示していないドキュメントをたまにみかけますが、 scheme やホストも含めてすべて示すか、 その説明にリンクするべきです。なぜなら、 読者 (開発者) は望む機能を探してエンドポイントの説明ページを見て、 それをもとにコードを書くので、その時必要な情報がそこにすぐにあるべきだからです。
[135] 驚くべきことに、ドキュメントのどこにも書かれていないサービスすら存在します (!)。 API 提供者は一度でもその API を実際に使って何かを作ってみようとしたことがあるのでしょうか。 (自分達は API を使わずになんでもできてしまうので、意外となかったりします。 しかしそんな態度で果たして使いやすい API が提供できるかどうか。)
[84] 可能な値の範囲は、できるだけ明確に文書化するべきです。その際は、 将来の拡張も視野に入れ、限定しすぎず、かといって一般化もしすぎず、 適当な粒度とするべきです。
[85] 例えば、あるサービスの利用者の識別子を API でやり取りする場合、 おそらくクライアントもその識別子を何らかの形で保存しなければなりませんから、 ストレージ上のデータ型を決めなければなりません。 識別子が数値なのか文字列なのか、含まれる文字の種類は何なのか、 最大の長さはどれだけなのか、といった情報がクライアント側開発者には有用です。
[86] ここで、サーバー側で識別子を32ビット整数型で表現しているとしても、 これをそのまま文書化することが望ましいとは限りません。将来の拡張の余地を残すため、 64ビットで表現可能な非負整数、と説明した方が良いかもしれません。
[87] 加えて、利用者の識別子は利用者によって互いに異なること、 決して他の利用者に再割当てされることがないこと、 といった性質も、説明しておくのが良さそうです。 一方で、識別子は小さい順に連番で割り当てられる、という情報は書かないべきでしょう (クライアント側で有用な情報ではなさそうですし、将来サーバー側で割当方法を変更したくなるかもしれません)。
[116] 「数値」や「タイムスタンプ」や「ISO 8601の日時形式」のような曖昧な説明ではなく、 「1個以上のASCII数字列によって表される十進整数」、 「HTML Standard が規定する妥当な大域日時文字列」 などの明確な説明とするべきです。
現実には残念ながらこれを怠る解説が少なくありません。 例えば「ISO 8601の日時形式」は無数の細かな選択肢がありますが、 それをすべて受け入れていることはほとんどありません。 技術ドキュメントは、異なる背景をもつ技術者同士が曖昧なく意思疎通できなければ意味がありません。
[117] 例示は、説明したことにはなりません。ドキュメントから例示をすべて取っ払ったときにも、 曖昧無く理解可能に記述するべきです。例示は理解の補助に過ぎません。
[133] クライアントの開発者がドキュメントの一部をコメントとしてソースコードに書き込んだり、 提示されたサンプルコードを改変して利用したりすることは、よくあります。 これを安心して行わせるために、ドキュメントは自由度の高いライセンスで提供するべきです。
[134] 具体的には、 CC0 や MITライセンスや Apacheライセンスが適切と思われます。
[136] ドキュメント中の重要な部分は、一般的な Webブラウザーで利用者が簡単にコピペできる状態になっているべきです。
[18] 提供されるサービスの性質次第ではありますが、クライアントの開発者が利用できる開発環境が提供されていると大変有用です。
[20] 課金など重大な処理が伴うものや、通常のサービス利用では前提条件を構築するのが難しい処理などは、適当な開発環境が用意されているべきです。
[19] API のホストを書き換える程度の手間で本番環境と切り替えられるのが理想的です。
[53] 有料サービスや審査制のものなどでは、開発者が容易にアクセスできる開発を提供することはとても重要です。 本格的な開発 (や契約その他) の前の評価段階の予備的開発のため、 特別な審査や費用などがなくても利用できるようにするべきです。
[54] プログラミング言語やライブラリーと同様に、 Web API にもこう設計するのが良い、悪いというような指針やコーディング規約のようなものが色々とあります。
[55] そうした指針類は、設計に迷った時のヒントとして有用かもしれませんが、 あまり意識しすぎても、無駄な制約でかえって開発しづらくなったり、 設計を歪めたりする危険性があります。
[56] 宗教性の高い指針には、特に自分の信仰と親和性が高そうに思った場合を除き、 従わない方が無難です。
[89] 有名サービスが採用している方針が、 常に好ましい方針とは限りません。真似る場合には、 自身の開発する対象にあわせて適切かどうかを都度検討するべきです。 長く続く有名サービスほど、歴史的な事情が積み重なっているはずです。 当該サービスの開発者自身も、おそらく何らかの不満を持っていることでしょう。
[90] 「宗教性」は、馴染みの薄い人には感じ取りにくいかもしれませんが、 次の各項に該当するなら要注意です。
[106] この指針自体も、ある種の「宗教性」が感じられるかもしれません。 信じるか信じないかはあなた次第です。
[126] Web は、新技術開発が活発に行われ続けている、変化の速い分野です。 設計方針は、時代の変化への追随の足枷になるべきではありません。
[127] 例えば、会社の意思決定の速度が遅い場合には、 会社の方針として設計の指針を定めるべきではありません。