1. sakuraya

    Posted

    sakuraya
Changes in title
+そのリクエストパラメータ、クエリストリングに入れますか、それともボディに入れますか
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,318 @@
+今回は、何らかのパラメータを扱うAPIを設計する際に、どこにパラメータを含めるべきかという問題について。
+
+選択肢は3つあります。
+
+**1. クエリストリングに含める**
+**2. リクエストボディに含める**
+**3. パスに含める**
+
+それぞれどんなユースケースに適しているのか例を挙げます。
+
+
+# 1. クエリストリングに含める
+何らかのリソースのフィルタリング、ソート、ページングを実現したいときに用います。
+**クエリ** と名前がついているくらいですからね。
+一覧や検索が主ですね。ゆえに GET 以外で見ることはほぼありません。
+ただし、認証のような例外もあります。これは後述します。
+
+## 検索
+
+```sh
+curl localhost:3000/items?name=hoge
+```
+
+## ソート
+
+```sh
+curl localhost:3000/items?sort_by=price&order=asc
+```
+
+## ページング
+
+```sh
+curl localhost:3000/items?page=3&limit=50
+```
+
+# 2. リクエストボディに含める
+リソースの作成や更新で必要な情報はここに入れます。
+ユーザーの入力値とかですね。
+
+## 作成
+
+```sh
+curl -X POST localhost:3000/items -d '{"name": "hoge", "price": 200}'
+```
+
+
+## 更新
+
+```sh
+curl -X PUT localhost:3000/items/1 -d '{"name": "fuga", "price": 400}'
+```
+
+```sh
+curl -X PATCH localhost:3000/items/1 -d '{"price": 500}'
+```
+
+
+# 3. パスに含める
+一意にリソースを特定できる識別子は必ずパスに含めましょう。
+IDとか商品コードとか注文番号とかですね。
+逆に複数リソースが該当する可能性がある場合は含めるべきではありません。
+
+## **商品コードABC-123の商品の情報を取得する**
+
+```sh
+curl localhost:3000/items/ABC-123
+```
+
+## **注文番号O-1239の注文のステータスを更新する**
+```
+curl -X PATCH localhost:3000/orders/O-1239 -d '{"status": "delivered"}'
+```
+
+
+# 指針
+つまりまとめるとこういうことです
+
+```ruby
+if "一意にリソースを特定できるユニークな識別子を持っている"
+ "=> パス"
+elsif "リソースを作成または変更するPOST/PUT/PATCHリクエストである"
+ "=> リクエストボディ"
+elsif "リソースのフィルタリング/ソート/ページングを行うGETリクエストである"
+ "=> クエリストリング"
+end
+```
+
+大体はこれに従って作っていけば違和感がない REST API になります。
+正しいHTTPメソッドを使っていることが前提になりますがね。
+GETでリソースの作成、削除などを行なっているAPIは、まずそこを変更しましょう。
+
+そしてこのフローチャートには例外が一つあります。
+それは **HTTPヘッダー** の存在です。
+そのパラメータが仕様として標準化されている HTTPヘッダー に該当する場合、
+上に挙げた3つのどこかよりも、HTTPヘッダーに含める方が適切になります。
+また、標準化されていないものでも、独自のHTTPヘッダーとして含める方がスッキリする場合もあります。
+これについての詳細は後述します。
+
+# 考え方
+なぜ前述のフローチャートのような考え方が適切なのか、
+その理由となる REST の思想的な話をします。
+
+## URLとは住所である
+REST において、 URLとは、一意に特定できるリソースの表現とイコールになります。
+よくURLはアドレス(=住所)のメタファーで説明されますが、
+引越しを行わない限りそのURLが表すものはずっと同じで、特定の何かになります。
+
+例えば、 `/books/1234` という、ある本の情報を取得するAPIエンドポイントがあるとします。
+1234は本一冊一冊につけられたユニークな管理コードです。
+1234というコードがつけられた本はこの一冊だけで重複はしません。
+
+`/books/1234` は、この本の住所であり、ここにアクセスすれば必ずこの本の情報が得られます。
+管理コード5678の本が出てくることは決してありません。
+
+
+ゆえに、ユニークなコードがパラメータとして扱われる場合は、パスに含めることになるわけです。
+そして、逆にユニークでないものをパスに含めるのは不適切な設計になります。
+
+試しに、本のタイトルをパスに含めたアンチパターンを見てみましょう。
+
+```
+/books/進撃の巨人
+```
+
+このURLを、タイトルが「進撃の巨人」と **完全一致する** 本の詳細を表すエンドポイントとして設計したとします。
+これはリソースを特定できていません。
+1巻, 2巻といった情報がないので複数の本が含まれてしまうし、同名のタイトルの小説も存在するかもしれません。
+
+そのため本の場合は、ISBNのようなユニークなコードを振って管理しているわけですね。
+
+例えば以下のようなコードが割り振られている場合、
+
+| タイトル | 備考 | コード |
+|:-----------------|:------------------|:------------------|
+| 進撃の巨人 | 1巻 | ABC-1 |
+| 進撃の巨人 | 2巻 | ABC-2 |
+| 進撃の巨人 | 小説 | DEF |
+
+それぞれを表現する適切なURLは以下になります。
+
+```
+/books/ABC-1
+/books/ABC-2
+/books/DEF
+```
+
+
+「山田太郎」というだけではどの山田太郎さんか特定できないけれど、
+どこに住んでいる山田太郎さんか、住所の情報があれば一意に特定できるようになるようなイメージです。
+
+## 動的なのはコンテンツで、URLは常に静的である
+「URLはユニークなリソースを表す」と言うと、
+複数のリソースを返す一覧ページに関して違和感を覚える人もいるかと思います。
+
+例えば Amazon で何か商品を検索するとき、
+検索結果は常に変わります。
+新しい商品が発売されれば結果は増えるし、
+どれだけ上の方に表示されるかは、売上のランキングなどにもよって変動的であるでしょう。
+
+常に同じ結果が返らないのであればそれはユニークとは言えないのではないか?と思うかもしれませんが、
+それは先ほどの詳細ページの例と、一覧ページを同じように扱っているからかもしれません。
+
+詳細の例では、必ず結果は1つで、複数のリソースが該当することはありませんでした。
+対して一覧のAPIは、複数のリソースを表現しています。
+とはいえ、その上で、URLは相変わらずユニークです。
+
+どういうことかというと、
+`/books` は、 `全ての本` を表すユニークなリソースであり、その意味合いは不変でユニークだということです。
+中身のコンテンツがどうあろうと、URLとその表現するリソースは常に同一です。
+
+ある会社の社員を表すURLが `/company/1/employees` だとします(※1は会社のID)。
+社員が退職したとしても、このURLが表すリソースは常に「この会社の全ての社員」です。
+新しく社員が増えても意味合いは変わりません。
+
+考えてみればシステムのデータというのは、流動的で常に変化していくものです。
+そのコンテンツ1つ1つに名前をつけていてはキリがありません。
+そこであるスコープで区切って住所(URL)を与えることで、 **リソース** を表現しているわけです。
+
+検索のパラメータがパスに位置しないのはそういうわけでもあります。
+
+`タイトルに「進撃の巨人」を含む本全て` を `/books/進撃の巨人` としてしまうと、
+存在するかどうかもわからない複数のリソースに住所を与えてしまうことになります。
+検索されうるタイトルは数限りなくあるわけで、その一つ一つに住所を与えるのは非常に非効率です。
+
+そうではなく、`全ての本` を表す `/books` で得られるリソースに対して、
+`/books?title=進撃の巨人` のように、タイトルでフィルタリングをかけていくわけです。
+
+## HTTPメソッドとリクエストボディの関係
+システムの機能のほとんどは、取得/作成/更新/削除のいずれかに分類することができます。
+これらをそれぞれ対応するHTTPメソッドとひもづけるのがAPI設計の一つの勘所です。
+
+そしてHTTPメソッドが決まれば、おのずとパラメータを埋め込む適切な位置は決まります。
+
+| HTTPメソッド | クエリストリング | リクエストボディ | パス |
+|:-----------------|:------------------:|:------------------:|:------------------:|
+| GET | ◎ | × | ○ |
+| POST | × | ◎ | ○ |
+| PUT | × | ◎ | ○ |
+| PATCH | × | ◎ | ○ |
+| DELETE | × | × | ◎ |
+
+わかりづらくて恐縮ですが、◎が適切な位置、○がものによって埋め込んでもいいもの、×が適切ではない位置となります。
+
+○に関しては、前述したような、リソースを一意に定義するユニークIDが対象です。
+これはまごうことなくパスに含めるべきです。
+それ以外の、ユーザー入力値などはリクエストボディに含めるのが自然です。
+
+このあたりは[RFC7231](https://tools.ietf.org/html/rfc7231) (Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content) が詳しいのですが、
+
+> The purpose of a payload in a request is defined by the method
+ semantics. For example, a representation in the payload of a PUT
+ request (Section 4.3.4) represents the desired state of the target
+ resource if the request is successfully applied, whereas a
+ representation in the payload of a POST request (Section 4.3.3)
+ represents information to be processed by the target resource.
+
+リソースの情報/状態を表すペイロードが相当します。
+
+対して、GET と DELETE に関しては、
+リクエストボディを含めると受け付けてもらえないかもよという記述があります。
+
+>A payload within a GET request message has no defined semantics;
+sending a payload body on a GET request might cause some existing implementations to reject the request.
+
+>A payload within a DELETE request message has no defined semantics;
+ sending a payload body on a DELETE request might cause some existing
+ implementations to reject the request.
+
+仕様として禁じてはいないのですが、ライブラリも対応していないものが多いので、
+こういうのを踏む恐れがあります。無闇に使わないのが無難です。
+[GETメソッドでリクエストボディを指定してはいけない(Swift)](https://qiita.com/uhooi/items/e82c8d294a8465a3e6f3)
+
+
+ちなみに HEAD は明確に禁じています。
+
+>The HEAD method is identical to GET except that the server MUST NOT send a message body in the response (i.e., the response terminates at the end of the header section).
+
+あとはクエリ文字列だとデータサイズの制限があるから...みたいに言う人がいますが、
+あまり本質的な理由ではありません。APIデザインとしてどこが適切かを考えるのが賢明です。
+
+このRFCを受けて、OpenAPI3.0でも、GET, DELETE, HEAD にリクエストボディを指定することはできなくなっています。
+
+
+## その DELETE 、本当に削除ですか?
+DELETEに関しては、基本的にID以外のパラメータが存在することはありません。
+なぜならこれはリソースの永続的な削除であり、他のいかなるフィールドも更新する必要がないからです。
+もしも、削除の理由などを入力するフィールドがあり、それをデータとして保存しておく必要があるのであれば、
+それは削除ではなくリソースの**更新**ではないか、設計を見直す必要があります。
+
+また、削除は削除でも、物理削除ではなく論理削除を採用しているシステムも珍しくはありません。
+リソースを削除フラグや削除日時で更新するような処理を、あえてDELETEとして設計することも許容はできるかと思います。
+それにしてもフラグや日時はサーバー側で処理すればいいだけの話で、パラメータとして渡す必要はありません。
+DELETEを使ったAPIでパラメータを渡さなければいけない場面があるとすれば、それはRESTfulではない可能性が高いです。
+
+
+一番避けるべきは GET で削除機能を実現することです。
+Googleなどのクローラーがリンクをたどってコンテンンツを削除していく話は有名です。
+読み取り専用であるGETは **安全な** メソッドとして位置づけられています。
+
+>4.2.1. Safe Methods
+ Request methods are considered "safe" if their defined semantics are
+ essentially read-only; i.e., the client does not request, and does
+ not expect, any state change on the origin server as a result of
+ applying a safe method to a target resource. Likewise, reasonable
+ use of a safe method is not expected to cause any harm, loss of
+ property, or unusual burden on the origin server.<br>
+RFC7231より
+
+## メタ情報はヘッダでまかなえることが多い
+[RFC2616](https://tools.ietf.org/html/rfc2616) をはじめとしてHTTP標準とされているヘッダに適切なものがあれば積極的に活用しましょう。
+
+例えば、「この言語で欲しい!」というのをリクエストする場合、
+クエリストリングで `lang=en` とするのはイマイチです。
+`Accept-Language` ヘッダに格納しましょう。
+
+また、標準でなくても、
+コンテンツのデータに関係のないメタ情報は独自ヘッダ `X-***` を定義して、
+データ部分と分離するのもよい設計だと思います。
+
+
+## 認証
+GET でクエリ文字列を使用するのはフィルタリングなどと書きましたが、
+認証情報を含めるケースもよく見かけます。
+
+例えばAWSのS3は、クエリストリングに含める手段も提供しています。
+[クエリ文字列による代替リクエスト認証](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth)
+
+Google APIも、APIキーをクエリ文字列に含めるパターンが多いです。
+[Google Map API](https://developers.google.com/maps/documentation/maps-static/intro?hl=ja)
+
+これもメタ情報なのでヘッダーか文字列に含めるのが定石ですが、特に決まりはないように思います。
+ブラウザからアクセスできるのでクエリストリングの方が何かと利便性高いなーとは感じます。
+
+ただパスワードのような秘匿情報はクエリストリングに含めるべきではないという主張があるので注意が必要です。
+非SSLの通信の場合丸見えになるとか、アクセスログに残るとか、etc.
+これについては長くなりそうなのでまたの機会に書きます。
+
+# まとめ
+前半に書いたフローチャートを書き直しました。
+
+```ruby
+
+if param == "一意にリソースを特定できるユニークな識別子である"
+ # => パス
+elsif param == "リソースを作成または更新する際の、リソースの情報/状態を表す値である"
+ # => リクエストボディ
+elsif param == "リソースのフィルタリング/ソート/ページングを表す"
+ # => クエリストリング
+elsif param == "認証情報である"
+ # => クエリストリング または ヘッダー
+elsif param == "コンテンツのデータには直接関係しないメタ情報である"
+ # => ヘッダー
+end
+```
+
+
+パラメータの設計は、適切なHTTPメソッドとURLの設計と切り離せない問題です。
+迷ったらそちらの角度からも眺めてみたりするといい感じの落とし所が見つかるかもしれません 🎉