LoginSignup
424
364

More than 3 years have passed since last update.

そのリクエストパラメータ、クエリストリングに入れますか、それともボディに入れますか

Last updated at Posted at 2019-12-07

今回は、何らかのパラメータを扱うAPIを設計する際に、どこにパラメータを含めるべきかという問題について。

選択肢は3つあります。

1. クエリストリングに含める
2. リクエストボディに含める
3. パスに含める

それぞれどんなユースケースに適しているのか例を挙げます。

1. クエリストリングに含める

何らかのリソースのフィルタリング、ソート、ページングを実現したいときに用います。
クエリ と名前がついているくらいですからね。
一覧や検索が主ですね。ゆえに GET 以外で見ることはほぼありません。
ただし、認証のような例外もあります。これは後述します。

検索

curl localhost:3000/items?name=hoge

ソート

curl localhost:3000/items?sort_by=price&order=asc

ページング

curl localhost:3000/items?page=3&limit=50

2. リクエストボディに含める

リソースの作成や更新で必要な情報はここに入れます。
ユーザーの入力値とかですね。

作成

curl -X POST localhost:3000/items -d '{"name": "hoge", "price": 200}'

更新

curl -X PUT localhost:3000/items/1 -d '{"name": "fuga", "price": 400}'
curl -X PATCH localhost:3000/items/1 -d '{"price": 500}'

3. パスに含める

一意にリソースを特定できる識別子は必ずパスに含めましょう。
IDとか商品コードとか注文番号とかですね。
逆に複数リソースが該当する可能性がある場合は含めるべきではありません。

商品コードABC-123の商品の情報を取得する

curl localhost:3000/items/ABC-123

注文番号O-1239の注文のステータスを更新する


curl -X PATCH localhost:3000/orders/O-1239 -d '{"status": "delivered"}'

指針

つまりまとめるとこういうことです

if "一意にリソースを特定できるユニークな識別子を持っている"
  "=> パス"
elsif "リソースを作成または変更するPOST/PUT/PATCHリクエストである"
  "=> リクエストボディ"
elsif "リソースのフィルタリング/ソート/ページングを行うGETリクエストである"
  "=> クエリストリング"
end

大体はこれに従って作っていけば違和感がない REST API になります。
正しいHTTPメソッドを使っていることが前提になりますが。
もしもGETでリソースの作成/削除などを行なっていたら、まずは
そこを見直しましょう

そしてこのフローチャートには例外が一つあります。
それは 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=進撃の巨人 のように、タイトルでフィルタリングをかけていくわけです。

/books 「とりあえず俺は全ての本を表現する住所だから全部あげるよ、それからフィルタリングするのは自由だけどね」みたいなスタンスです。

HTTPメソッドとリクエストボディの関係

システムの機能のほとんどは、取得/作成/更新/削除のいずれかに分類することができます。
これらをそれぞれ対応するHTTPメソッドとひもづけるのがAPI設計の一つの勘所です。

そしてHTTPメソッドが決まれば、おのずとパラメータを埋め込む適切な位置は決まります。

HTTPメソッド クエリストリング リクエストボディ パス
GET ×
POST ×
PUT ×
PATCH ×
DELETE × ×

わかりづらくて恐縮ですが、◎が適切な位置、○がものによって埋め込んでもいいもの、×が適切ではない位置となります。

○に関しては、前述したような、リソースを一意に定義するユニークIDが対象です。
これはまごうことなくパスに含めるべきです。
それ以外の、ユーザー入力値などはリクエストボディに含めるのが自然です。

このあたりは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)

ちなみに 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.

RFC7231より

メタ情報はヘッダでまかなえることが多い

RFC2616 をはじめとしてHTTP標準とされているヘッダに適切なものがあれば積極的に活用しましょう。

例えば、「この言語で欲しい!」というのをリクエストする場合、
クエリストリングで lang=en とするのはイマイチです。
Accept-Language ヘッダに格納しましょう。

また、標準でなくても、
コンテンツのデータに関係のないメタ情報は独自ヘッダ X-*** を定義して、
データ部分と分離するのもよい設計だと思います。

認証

GET でクエリ文字列を使用するのはフィルタリングなどと書きましたが、
認証情報を含めるケースもよく見かけます。

例えばAWSのS3は、クエリストリングに含める手段も提供しています。
クエリ文字列による代替リクエスト認証

Google APIも、APIキーをクエリ文字列に含めるパターンが多いです。
Google Map API

これもメタ情報なのでヘッダーか文字列に含めるのが定石ですが、特に決まりはないように思います。
ブラウザからアクセスできるのでクエリストリングの方が何かと利便性高いなーとは感じます。

ただパスワードのような秘匿情報はクエリストリングに含めるべきではないという主張があるので注意が必要です。
非SSLの通信の場合丸見えになるとか、アクセスログに残るとか、etc.
これについては長くなりそうなのでまたの機会に書きます。

まとめ

前半に書いたフローチャートを書き直しました。


if param == "一意にリソースを特定できるユニークな識別子である"
  # => パス
elsif param == "リソースを作成または更新する際の、リソースの情報/状態を表す値である"
  # => リクエストボディ
elsif param == "リソースのフィルタリング/ソート/ページングを表す"
  # => クエリストリング
elsif param == "認証情報である"
  # => クエリストリング または ヘッダー
elsif param == "コンテンツのデータには直接関係しないメタ情報である"
  # => ヘッダー
end

パラメータの設計は、適切なHTTPメソッドとURLの設計と切り離せない問題です。
迷ったらそちらの角度からも眺めてみたりするといい感じの落とし所が見つかるかもしれません 🎉

424
364
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
424
364