2021年6月6日日曜日

REST APIの特殊な場合のエンドポイント案

現在とあるウェブアプリケーションを作っているんですが、REST APIで特殊な条件でリソースを絞り込みたい場合があったので、「こうしたらええんちゃう」という考えをまとめてみました。多分特別あたらしいアイデアでもないです。誰かが似たようなことか、もっといいことをすでに書いていると思います。

一応言っておきますが、これはただの案で正解とは限りません。いろいろ試行錯誤した結果、これが個人的にわかりやすい気がしたというだけです。

REST APIの基本的な設計は理解している前提で書きますので、基本設計が不安な方は適当に調べておいてください。

まずは基本

本題に入る前にちょっと準備運動。

主キーは符号なし64ビット整数で統一

まずはAPI自体の話ではありませんが、データベース側の主キーの話。

主キーには自然キーを使うべきか人工キーを使うべきかについては以前記事を書いたので、そちらを参照して人工キーを使ってください。オートインクリメントが使えるなら通常は64ビット整数で十分です。

ただし、秒間100万件のレコードを挿入し続けるとたった30万年で64ビットを使い切ってしまうので、30万年以上使う予定のあるシステムならもっと大きなサイズにしたほうがいいかもしれません。

リソースIDにはUUIDを使う

次はリソースIDについて。リソースIDは主キー(64ビット整数)にしてしまえば余計なカラムを追加せずに済みますが、主キーを外部に晒すことが好ましくない場合もあります。なのでUUIDカラムを作って外部からはこのUUIDでアクセスするようにしましょう。

たとえばECサイトを構築したとします。注文管理テーブルの主キーを上記のようにオートインクリメントの整数にして、APIのリソースIDにもこの主キーを使った場合、店がどれくらい繁盛しているか購入者にわかってしまいます。 例えば今日注文したときの注文番号が10で、1年後にもう一度注文したときに注文番号が15だったら、1年間に他に4人しか注文者がないことが外部から推測できてしまいます。

他には、JavaScriptで64ビット整数が扱えないという問題もあります。ウェブアプリケーションの場合はクライアントはJavaScriptを使わざるを得ないわけですが、JavaScriptではNumber.MAX_SAFE_INTEGERより大きい数値を扱えません。まあそれでも使い切るには秒間1万件入力し続けて2万8千年以上かかるので、ほとんどの場合実用上は問題ないんですが。

応用編

いよいよ本題です。

「現在ログイン中のユーザー」に関するエンドポイントはチルダ(~)を使う

リソースID=4f284fac-ee91-4a77-9f55-ddfc43fe048bのユーザー情報を取得する場合のエンドポイントは GET /users/4f284fac-ee91-4a77-9f55-ddfc43fe048b ですが、このユーザーが現在システムにログインしていて、自分自身のユーザー情報(認証情報はCookieやAuthorizationヘッダーで渡している)を取得したい場合はリソースIDを使わず GET /users/~ を使いましょう。

なんでチルダかというと、Unix系OSではチルダはログイン中のユーザーのホームディレクトリーを指すことからの連想です。

GET /user のように複数形ではなく単数形を使うのも悪くないかもしれませんが、間違えやすそうだと思ったのでこちらは採用していません。

アクセス権限によってエンドポイントを分ける

「誰でも取得できるユーザー情報」と「自分自身が取得できるユーザー情報」と「システム管理者が取得できるユーザー情報」は必要な情報も権限も違うので、エンドポイントを分けましょう。

エンドポイントを分けずに、例えば   GET /users/4f284fac-ee91-4a77-9f55-ddfc43fe048b

  • ログインしていない人や他のIDでログインしている人がアクセスしたら、公開されている最低限の情報のみ返す
  • 自分自身がアクセスしたら、↑に加えて非公開情報を含めた自分が登録した情報も返す
  • システム管理者がアクセスしたら、↑に加えて管理用の情報(「こいつはクレームが多いから注意」みたいなメモとか)も返す

のように認証情報によって返す情報を変えるような仕様にしてしまうと、仕様もロジックも複雑になってバグを入れてしまう可能性があります。管理者用の情報を誰でも閲覧できるバグなんか入れた日にはお祭り騒ぎです。

  • 特定のユーザーの公開情報を取得: GET /users/4f284fac-ee91-4a77-9f55-ddfc43fe048b
  • 自分自身の情報を取得: GET /users/~
  • システム管理者が特定のユーザーの情報を取得: GET /administrators/~/users/4f284fac-ee91-4a77-9f55-ddfc43fe048b

のように、アクセス権限によってエンドポイントを分ければ、役割が明確になり、認証処理も入れやすくなります。

特殊な絞り込みは、先頭に@を使う

リソースを特定の条件で絞り込みたい場合があります。たとえばTwitterの「認証済みユーザー」のようなもの。普通はクエリーストリングなどで絞るんですが、よく使われる条件の場合はショートカットがあると便利だったりします。

そんなときは、GET /users/@verified のように「@ショートカット名」をつけましょう。

@を先頭につけることでリソースIDではないということが明確になりますし、「@がついているものは特殊なショートカットで、戻り値はリソースの配列」というルールもつけやすくなります。

ちなみにRFC3986によると、@はパス名に問題なく使えます。一安心。

リソースID以外でのリソース指定について

特定のリソースを取得する場合は、原則としてUUIDによるリソースIDを使います。しかし、状況によってはリソースID以外の一意な情報を使ってリソースを指定したい場合があります。

例えばシステム管理者がメアドやログインIDを使ってユーザーを指定したいような場合は、こんなエンドポイントを作りましょう。

  • GET /administrators/~/users/email=foo@example.com
  • GET /administrators/~/users/loginId=foo

@と同様に、=を使うことでリソースIDではないということが明確になりますし、「=がついているものは特殊なリソース指定で、戻り値はリソース情報(見つからなければ404)」というルールもつけやすくなります。

RFC3986によると、=もパス名に使えます。

login_id(スネークケース)かloginId(キャメルケース)か」については以前の記事をごらんください。今作っているAPIではキャメルケースを使っています。「ドメイン」「パス」「クエリーストリング」「JSONオブジェクトのキー名」のすべてで使え、統一感を出せるのが理由です。

おわりに

というわけで、今回は勝手にREST APIの設計について語ってみました。最初に書いたとおり、もっといい方法がすでに発表されているかもしれません。

個人的なベストプラクティスのようなものと捉えてください。

0 件のコメント:

コメントを投稿