Google Cloud Platform

API デザイン : URL には名前と識別子のどちらを使うべきか

ウェブ API の設計に携わっている方であれば、API で使う URL のスタイルに統一的な考え方がないことも、選択した URL スタイルが API の使いやすさや寿命に大きな影響を与えることも、よくご存じでしょう。Google Cloud の Apigee チームは、社内だけでなくお客様とも協力しながら、API の設計について長く検討を行ってきました。本稿では、私たちが設計の現場で実際に使用している URL のデザイン パターンと、それを使う理由についてシェアしたいと思います。

著名なウェブ API をご覧になれば、いくつかの異なる URL パターンがあることに気づかれるはずです。次に示すのは、極端に異なる考え方に基づいた 2 つのスタイルの具体例です。

https://ebank.com/accounts/a49a9762-3790-4b4f-adbf-4577a35b1df7
https://library.com/shelves/american-literature/books/moby-dick

1 番目の URL は、私が当座預金口座を開設している米国の銀行の実際の URL を匿名化して単純にしたものです。一方、2 番目の URL は Google Cloud Platform API Design Guide に掲載されている教育用サンプルを修正したものです。

1 番目の URL はわかりにくい感じです。銀行口座の URL だということは推測できるでしょうが、それ以上のことはわかりません。16 進文字列の記憶に人並み外れた能力があるのでもないかぎり、この URL は簡単には入力できないでしょう。

ほとんどの人は、この URL にたどり着くために、コピー&ペーストもしくはリンクのクリックを使うはずです。16 進文字列の記憶力が私並に低い方にとっては、この種の 2 つの URL が同じものか異なるものかをひと目で見分けることは難しいでしょうし、ログ ファイル内で同じ URL が使われている箇所を探すことも簡単ではないでしょう。

それに対して 2 番目の URL はずっと透明性が高いものです。記憶、入力、他の URL との比較も簡単です。また、URL 自体にちょっとしたストーリーが読み取れます。いわば、名前を付けられた棚に名前のある本が置かれているといったところです。この URL は簡単に自然言語文に翻訳できます。

それでは、どちらのスタイルを使うべきでしょうか。一見すると 2 番目の URL が望ましいように思えますが、本当の答えはもっと微妙で複雑です。

識別子の長所

数値または英数字による識別子をエンティティに与えることは古くから行われており、コンピュータよりも長い伝統があります。銀行や保険会社は口座や規定文書に識別子を割り当て、製造会社や卸売/小売業者は自社製品を製品コードで識別しています。本は ISBN で管理され、政府は社会保険番号や運転免許証番号、犯罪事件番号、土地区画番号などを発行しています。

1 番目の URL の例は、こうした方法を単にワールド ワイド ウェブの URL 形式で表現したものです。しかし、この種の識別子には上述したような欠点(判読や比較、記憶、入力が難しい、識別しているエンティティについての有用な情報がないなど)があります。では、なぜ私たちはこういうものを使うのでしょうか。

最大の理由は、物事の変化に伴い安定性や確実性が非常に重要になる局面が訪れたとしても、識別子の形式であれば、そこに曖昧さが入り込む余地はなく、有効であり続けるからです(Tim Berners-Lee 氏が、このテーマについてよく引用される論文を書いています)。

銀行口座に識別子を割り当てていない場合、将来にわたって確実に口座にアクセスしたいときはどうすればよいのでしょうか。既知の情報を利用して口座を識別しようとしても、その情報が変化していれば口座を一意に識別できないため、情報による識別は確実な方法にはなりえません。

口座の名義人に関する各種情報は、いずれも変更されるか(たとえば名前、住所、既婚/未婚)、あるいは曖昧さを残すか(たとえば誕生日、出生地)、もしくはその両方となります。名義人を識別するための信頼できる情報を持っていても、口座の名義人自体は変わることがあります。また、口座がいつどこで作成されたかで識別しようとしても、一意性は保証されません。

階層構造の名前

階層構造に対応した命名は、人類が何世紀も前から使ってきた非常に強力なテクニックであり、世界とは何かを理解できる方法です。Carolus Linnaeus が1700 年代に編み出した自然の分類法は、その例として非常に有名です。

2 番目の例のようなスタイルの URL(単純な名前の階層構造によって構成されるもの)は、この考え方を基礎としています。この種の URL は、単純な数値や英数字による識別子とは対照的な性質を持っています。人間にとって使いやすく、組み立てやすく、情報が得やすい一方で、変化に対する安定性に欠けています。

Linnaeus の分類法を少しでも知っていれば、時間の経過とともに分類の要素名が変化し、階層構造が大きく作り直されていることもご存じでしょう。実際、DNA 分析などの最新テクノロジーが採用されるようになってからは、変化のペースが上がっています。

ほとんどの命名方式では変えられるということが非常に重要であり、名前が変化しないことを前提とした設計には疑問を感じるはずです。私たちの経験から言えば、名前を変更し、名前の階層構造を再構成できることは、API を最初に設計した人々からすれば予想外であったとしても、ほとんどのシナリオにおいてとても重要であり、望ましいことです。

2 番目の URL の欠点は、本や本棚の名前に変更が生じたときに、例にあるような階層構造の名前のままでは本を参照できなくなることです。大量に印刷された文献の中の 1 冊の名前が変わるということはまず考えられませんが、図書館に所蔵されている文献では名前の変更はありうることですし、本棚の名前が変わることもごく普通に起きることです。同様に、本棚の間を本が移動することも珍しくなく、その場合はこの URL で参照できなくなります。

以上のことから、一般的な原則が見えてきます。不透明な識別子(パーマリンクと呼ばれることもあります)による URL は、本質的に安定していて確実ですが、あまりヒューマン フレンドリーではありません。それに対して、名前の階層構造のように人間にとってわかりやすい情報から URL を組み立てれば、ヒューマン フレンドリーなものになります。

ただし、後者の場合は、エンティティの名前を変えたり階層構造を再編したりすることを禁止するか、もしくはこの種の URL がエンティティを参照できなくなったときの対処方法をあらかじめ用意しておく必要があります。

ここまでは、API で使う URL への影響という観点から識別情報のジレンマについて述べましたが、この問題は、データベースに格納されている識別情報や実装コンポーネント間でやり取りされている識別情報にも影響を与えます。

一般に、API のための URL は、API 実装がデータベースに格納している識別情報に基づいているため、URL と識別情報は互いに影響を与え合います。実装と URL が名前の階層構造を使って物を識別している場合、物を参照できなくなったときの結果や、名称変更、階層構造の再編などをサポートすることの難しさは複合的になります。別の言い方をすれば、この問題は API 設計だけではなく、システム全体の設計にとって重要になるということです。

両方の長所を取り入れるには

どちらのスタイルを選んでも一長一短あるなかで、さてどちらを選ぶべきでしょうか。最良の答えは、選ばないことです。必要な機能を完全にサポートするには両方が必要です。両方のスタイルの URL を用意すれば、API は安定した識別子と使いやすい階層名の両方を手に入れられます。

Google Cloud Platform(GCP)の API 自体は、名称変更や階層構造の再編がありうるエンティティでは両方のタイプの URL をサポートしています。たとえば GCP プロジェクトは、安定したパーマリンク URL に組み込まれる不変の識別情報と、検索で使える可変の名前の両方を持っています。私の GCP プロジェクトを例にとると、その中の 1 つは、“bionic-bison-166600” という識別情報(これは、RFC 準拠の UUID のように暗号めいたものである必要はありません。単に安定していて一意であればよいのです)と、今のところ “My First Project Renamed” となっている名前の両方を持っています。

識別子はルックアップ用、名前は検索用

すべての URL が特定のエンティティを識別することは、ワールド ワイド ウェブの原則です。したがって、https://ebank.com/accounts/a49a9762-3790-4b4f-adbf-4577a35b1df7 が特定の銀行口座の URL であることは明らかでしょう。現在であれ将来であれ、私がこの URL を使用すれば必ず同じ銀行口座が参照されます。

一方、“https://library.com/shelves/american-literature/books/moby-dick” はどうでしょうか。特定の本の URL だと思われるかもしれません。仮説としてでも、図書館の API で本の名前の変更や本棚の移動はありえないと考えるなら、これを本の URL と見なしてもよいでしょう。しかし、変更や移動はありうると考えるなら、この URL は別のものだと判断しなければなりません。

今この URL にアクセスすると、参照されるのは、アメリカ文学の棚にある、ページの隅が折れ曲がった特定の『白鯨』の本です。しかし、明日になってこの本や本棚が移動されたり名前が変えられたりしたら、URL が参照するのは新たに購入された『白鯨』の本かもしれませんし、該当する本がない可能性もあります。

このように考えると、後者のタイプは特定の本の URL ではないことがわかります。何か他のものの URL だと考えなければなりません。この種の URL は検索結果だと考えるべきです。具体的には、次のような検索の結果です。

(現時点で)『白鯨』(moby-dick)という名前で、(現時点で)「アメリカ文学」(american-literature)という名前の本棚に(現時点で)ある本を探せ

次に示すのは、同じ検索結果を参照する別の URL です。違いは URL のスタイルであって、意味に違いはありません。

https://library.com/search?kind=book&name=moby-dick&shelf=(name=american-literature)

名前の階層構造をベースとする URL は、実際には検索結果の URL であり、検索結果に含まれるエンティティの URL ではないと考えてください。これを理解することは、名前と識別子の違いを説明するうえで重要なポイントになります。

名前と識別子の併用

パーマリンクと検索 URL を併用するためには、まず個々のエンティティにパーマリンクを与えます。たとえば新しい銀行口座を作るときは、口座開設に必要な情報を https://ebank.com/accounts に POST します。口座開設処理に成功すると、銀行の API は、ステータス コードが 201 で、HTTP の “Location” ヘッダを含むレスポンスを返します。Location ヘッダの値は、新しい口座の URL である https://ebank.com/accounts/a49a9762-3790-4b4f-adbf-4577a35b1df7 です。

図書館用の API を設計するときも同じパターンに従います。まず、次のリクエスト本体を含む POST リクエストを https://library.com/locations に送ります。

  {"kind": "Shelf",
 "name": "American-Literature",
}

その結果、本棚用に次の URL が割り当てられます。

https://library.com/shelf/20211fcf-0116-4217-9816-be11a4954344

次に、本のエンティティを作るため、次のリクエスト本体を https://library.com/inventory に POST します。

  {"kind": "Book",
 "name": "Moby-Dick",
 "location": "/shelf/20211fcf-0116-4217-9816-be11a4954344"
}

すると、本のために次の URL が割り当てられます。

https://library.com/book/745ba01d-51a1-4615-9571-ee14d15bb4af

この安定した URL は、私がどう呼び出すか、あるいはどの本棚に置くかにかかわらず、特定の『白鯨』の 1 冊を参照します。本が紛失したり破けたりしても、この URL は常に同じものを識別します。

このエンティティに対しては、次の検索 URL も有効であることが考えられます。

https://library.com/shelf/american-literatature/book/moby-dick
https://library.com/search?kind=book&name=moby-dick&shelf=(name=american-literature)

時間と労力があれば、同じ API で両方の検索 URL スタイルを実装してもよいでしょう。それが難しいときは、好みのスタイルを選んで、それを使い続けるようにします。

クライアントがこれらの検索 URL のどちらかを指定して GET リクエストを送るたびに、見つかったエンティティの識別 URL(パーマリンクを指し、この場合は https://library.com/book/745ba01d-51a1-4615-9571-ee14d15bb4af)を含んだレスポンスを返すようにします。識別 URL は、ヘッダ(HTTP Content-Location ヘッダはこの目的のために存在します)、もしくはレスポンス本体、できれば両方に含めるようにします。こうすることでクライアントは、同じエンティティを指すパーマリンク URL と検索 URL の間を自由に行き来できます。

2 種類の URL を持つ方法の欠点

どんな設計にも欠点があります。同じ API でパーマリンク URL と検索 URL の両方をサポートすることについても、明らかに努力がもう少し必要です。

より重要な問題として、どのような状況でどちらの URL を使うべきかをユーザーに教える必要もあります。通常は検索 URL を使ってかまいませんが、データベースに URL を格納するときは(単にブックマークを作るときでも)、識別 URL(パーマリンク)を使用しなければなりません。

また、識別子の格納にも注意が必要です。API 実装によって永続的に保存されるべき識別子は、ほとんどの場合、パーマリンクを作るために使用された識別子です。データベース内の参照や識別情報を名前で表すことは稀であり、そのような形で使われている名前をデータベース内で見つけたときは、その使い方を慎重に検討すべきでしょう。

API にアクセスするスクリプトを書くときには、検索 URL かパーマリンク URL かを自由に選べます。すでに知っている名前や値から簡単に組み立てられる検索 URL のほうが手っとり早くて簡単ですが、API レスポンスのヘッダや本体からパーマリンク URL をパースして取り出すのに少しだけ余計に時間が必要です。

スクリプトで検索 URL を使うときの問題点は、API エンティティの名前が変更されたり、階層構造内で移動したりしたときにスクリプトが動かなくなることです。これは、ファイルの名前が変わったり場所が移動したりするとスクリプトが動かなくなるのと同じです。ファイル名の変更に合わせてスクリプトを修正することには慣れているでしょうから、検索 URL の使用中にスクリプトが動かなくなったら修正を加えるということで問題はないでしょう。ただし、スクリプトの確実性や安定性が特に重要なケースでは、パーマリンクを使ってスクリプトを書くべきです。

パーマリンクと検索 URL の “いいとこ取り”

データの変更を非常に厳しく制限しないかぎり、1 種類の URL セットだけで、安定かつ確実で使いやすい API を実現することはできません。最良の API は、安定した識別を可能にするパーマリンク URL と、使いやすい名前ベース(おそらく他の値も使用した)の検索 URL の両方を持ちます。

API 設計の詳細は、『Web API Design : The Missing Link』(e ブック)や、Apigee ブログにおける API 設計関連の投稿記事をご覧ください。

* この投稿は米国時間 10 月 17 日、software developer および API designer である Martin Nally によって投稿されたもの(投稿はこちら)の抄訳です。

- By Martin Nally, software developer and API designer