先日、HTTPメソッドに QUERY が追加された、という話を見かけた。第一印象は、GETにBodyを付けられるようになったのかな、くらいのものだった。予想通りと言えば予想通りである。ただ、実務的にどういう意味があるのかが気になったので、重い腰を上げて調べてみた。

見た目はこういう代物である。

QUERY /orders HTTP/1.1
Content-Type: application/json

{
  "status": ["paid", "completed"],
  "createdAt": {
    "from": "2026-07-01",
    "to": "2026-07-31"
  }
}

ざっくり言えば、Body付きで安全な問い合わせをするためのHTTPメソッドだ。2026年6月に RFC 10008 として標準化されている。HTTPのリクエストは、クライアントからサーバーまでの道中でいくつもの関所を通る。メソッドはそこで差し出す申告書のようなもので、その書式が一枚増えた、という話なのだ。

GETかPOSTで頑張っていた時代

これまで実務では、こういうAPIをよく作っていた(僕も何本書いたか覚えていない)。

POST /orders/search
Content-Type: application/json

{
  "status": ["paid", "completed"],
  "from": "2026-07-01",
  "to": "2026-07-31",
  "sort": {
    "field": "createdAt",
    "order": "desc"
  }
}

やっていることは検索なので、本来は読み取りに近い。とはいえ条件が複雑になると、URLに詰め込むよりリクエストボディで渡した方が扱いやすい。たとえば、特定のIDだけを取得したい場合もある。

{
  "includeIds": ["order_001", "order_002", "order_003"]
}

逆に、特定のIDを除外したいという条件もありうる。

{
  "excludeIds": ["order_999", "order_888"]
}

これをクエリパラメータだけで表現しようとすると、次のようになる。

GET /orders?includeIds=order_001&includeIds=order_002&includeIds=order_003

まだこの程度なら耐えられる。ところがIDの数が増えたり、他の検索条件と組み合わさったりすると、URLはみるみる読みにくくなっていく。

GET /orders?status=paid&includeIds=order_001&includeIds=order_002&excludeIds=order_999&sort=createdAt_desc

検索条件がネストすると、話は一段とややこしい。「この条件群のいずれかに一致する」「この条件は除外する」といった指定まで入ってくれば、クエリパラメータだけで表現するのはいよいよ苦しい。過去には、URLの長さ制限に引っかかったこともあった。

そこで実務では、POST /search のような形にして、検索条件をJSONで渡すことがあった。
POST /orders/search
Content-Type: application/json

{
  "includeIds": ["order_001", "order_002"],
  "excludeIds": ["order_999"],
  "filters": {
    "status": ["paid", "completed"],
    "createdAt": {
      "from": "2026-07-01",
      "to": "2026-07-31"
    }
  },
  "sort": [
    {
      "field": "createdAt",
      "order": "desc"
    }
  ]
}
この形はAPIとしてかなり扱いやすい。フロントエンドからは検索フォームの状態をそのままJSONにしやすく、バックエンドでもバリデーションしやすい。もっとも、いささか気持ち悪さが残るのも否めない。POST には一般に「作成」や「状態変更」のイメージが強い。実際には検索しているだけなのにメソッドは POST で、このズレがどうにも据わりが悪いのだ。

検索なのにPOSTでいいのか

POST /orders/search は実務では普通にある。ただ、POST は本来、なにかを作る、処理する、状態を変えるかもしれない、というメソッドとして使われることが多い。

むろん、POSTだから必ず状態変更する、というわけではない。検索用にPOSTを使っても、サーバー側で状態を変えなければ実害はない。問題は、クライアントやプロキシやキャッシュが、メソッドだけを見て安全な読み取りかどうかを判断しにくいことにある。関所の役人は、申告書の表書きしか読まないのである。

たとえばネットワークが途中で切れたとき、自動でリトライしてよいのか。キャッシュしてよいのか。再実行しても副作用は出ないか。GET なら、基本的には安全な読み取りだと判断できる。対して POST では、サーバー固有の事情を知らない限り判断がつかない。 つまり POST /search は便利だけれど、HTTPの意味としては少し曖昧だった。この曖昧さを埋めるために出てきたのが QUERY なのだと、僕は理解している。

Body付きの安全な問い合わせ

RFC 10008では、QUERY は次のようなメソッドとして定義されている。リクエストに含まれる内容を、リクエスト対象に対して safe かつ idempotent に処理し、その結果を返す。safeとは、ざっくり言えばサーバーの状態を変更しないことだ。idempotentは日本語で言う冪等性で、同じリクエストを何回実行しても、副作用の面で同じ性質になることを指す。 要するに QUERY は、POSTのようにBodyを送れるが、意味としては読み取り、問い合わせの側に寄っている。
QUERY /orders
Content-Type: application/json

{
  "status": ["paid", "completed"],
  "createdAt": {
    "from": "2026-07-01",
    "to": "2026-07-31"
  }
}
これはかなり素直に読める。/orders に対して、Bodyで指定した条件で問い合わせているだけだ。注文を作ったり更新したりしているわけではない。これまで POST /orders/search と書いていたものを、HTTPメソッドとしてもう少し正確に表現できるようになった。申告書に、ようやく正直な用件を書けるようになったわけだ。

safe method with body

面白いのは、RFCになる前のドラフト名が safe-method-w-body だったことだ。問題意識は最初からはっきりしていたのである。Bodyを持てるsafeなHTTPメソッドが欲しい、という話だ。 GETはsafeだが、複雑な条件をBodyに入れる用途には向いていない。POSTはBodyを持てるが、safeとは限らない。そして QUERY は、Bodyを送れて、かつsafeな問い合わせとして表現できる。GETとPOSTの間にあるユースケースは実務にずっと存在していて、QUERY はまさにそこを埋めようとしているのだ。

GET with Bodyではだめなのか

ここで自然に浮かぶ疑問がある。GET にBodyを付ければよかったのでは、というものだ。
GET /orders HTTP/1.1
Content-Type: application/json

{
  "status": ["paid"]
}

見た目だけなら、これでもよさそうに見える。ところがGETのBodyは、歴史的に扱いが不安定だ。実務的な事情はもっと単純で、サーバー、プロキシ、ライブラリ、ブラウザAPIなどが期待通りに扱ってくれるとは限らない。無視されることもある。途中のミドルウェアで落とされるかもしれないし、クライアントライブラリがそもそも送れないかもしれない。

キャッシュの観点でも難しい。GETは基本的にURIを中心に扱われるので、同じURIでBodyだけが違うリクエストの扱いは、実装によってずれが出やすい。GETの意味を無理に広げるより、Bodyを持てる安全な問い合わせメソッドを新しく定義した方が分かりやすい。それが QUERY という整理なのだろう。

SEARCHではなくQUERYという名前

名前としては SEARCH でもよさそうに見える。検索APIなら、SEARCH /orders の方が直感的かもしれない。 ただ、HTTPにはWebDAV由来の SEARCH など、既存のメソッドがある。それに、今回やりたいのは単なる検索ではない。対象リソースに対して、Body付きの問い合わせを行うことだ。そう考えると QUERY の方が守備範囲が広く、検索だけでなく集計、変換、絞り込み、問い合わせ言語の実行なども含められる。 たとえば次のようなものも、QUERY の範囲に入りうる。
QUERY /analytics
Content-Type: application/json

{
  "groupBy": "month",
  "metrics": ["sales", "orders"],
  "where": {
    "country": "JP"
  }
}
これは検索というより問い合わせに近い。その意味で、QUERY という名前はけっこう自然である。

Accept-Queryヘッダー

QUERY には Accept-Query というレスポンスヘッダーも定義されている。サーバーが「このリソースは、この形式のQUERYを受け付ける」と示すためのものだ。
HTTP/1.1 200 OK
Accept-Query: application/json
クライアントはこれを見れば、このリソースに application/json のBodyで QUERY を送れると分かる。実務でどのくらい使われるかは、今後の対応状況次第だろう。少なくとも仕様の側では、QUERYをどう発見させるかまで考えられている。

GETよりは難しいキャッシュ

QUERY はsafeでidempotentなので、キャッシュとの相性も意識されている。ただし、GETのように単純にはいかない。GETの場合、多くのキャッシュはURIを中心にキャッシュキーを作る。一方 QUERY は問い合わせ条件がBodyに入るので、同じ /orders 宛てでもBodyが違えば別の問い合わせになる。

たとえば、支払い済みの注文を問い合わせるリクエストがある。

QUERY /orders
Content-Type: application/json

{ "status": "paid" }

そしてこちらは、キャンセル済みの注文を問い合わせるリクエストだ。

QUERY /orders
Content-Type: application/json

{ "status": "cancelled" }
この二つは、同じURIでも違う結果になるはずである。そのため QUERY のキャッシュでは、URIだけでなく、リクエストBodyや関連するメタデータも考慮しなければならない。仕様上はキャッシュ可能でも、実装はGETより難しい。実務で使うときは、ここに注意が要る。

CORSのプリフライト

ブラウザから QUERY を使う場合は、CORSのプリフライトも考える必要がある。QUERY は、GET や POST の一部のような、いわゆるシンプルリクエストとしては扱われない。そのため別オリジンに送ると、OPTIONS リクエストが先に飛ぶ。API側で QUERY を受け付けるだけでなく、CORS設定でも QUERY メソッドを許可しなければならないわけだ。
Access-Control-Allow-Methods: GET, POST, QUERY, OPTIONS
本番の関所の手前で、もう一度身元を改められるようなものだ。このあたりも、QUERY を実務で使ううえでの小さくないハードルになる。

すぐ使うべきか

では、明日から全部 QUERY に変えるべきか。そこは慎重でいい。仕様としては標準化されたが、実務ではHTTPメソッドそのものより、周辺の対応状況の方がずっと効いてくる。 ブラウザ、fetchやaxiosなどのクライアント、API Gateway、CDN、WAF、リバースプロキシ、Webフレームワーク。OpenAPIなどのドキュメント生成に、ログ基盤、監視ツール。これらがすべて QUERY を問題なく通してくれる必要がある。HTTPメソッドとして正しくても、途中のどこかで未知のメソッドとして弾かれたら使いものにならない。リクエストが通り抜けるべき関所の数を数えると、なかなか壮観である。 とりわけ公開APIや、いろいろなクライアントから叩かれるAPIでは、まだ POST /search の方が無難な場面が多いだろう。未対応の関所が一つでも残っていれば、そこで足止めを食うからだ。

曖昧だった設計に名前がつく

QUERY の価値は、いますぐPOST検索を置き換えることにあるのではない。これまで曖昧だった設計に名前がついたことにある、と僕は見ている。

これまでの検索APIには、だいたい二つの選択肢があった。シンプルな検索なら、素直にGETで書けばよい。

GET /orders?status=paid

複雑な検索なら、実務上はPOSTで書くことが多かった。

POST /orders/search
Content-Type: application/json

{
  "status": "paid"
}
そしてこれからは、三つ目の選択肢として QUERY が加わる。
QUERY /orders
Content-Type: application/json

{
  "status": "paid"
}

整理すると、次のような分担になる。

やりたいこと向いているメソッド
URLで表現できる単純な取得GET
Body付きの安全な問い合わせQUERY
作成・更新・副作用のある処理POST
この整理はかなり分かりやすい。殊に、検索条件は複雑だが意味としては明らかに読み取りであるAPIには、QUERY がよく合う。

REST API設計として見ると

REST APIの設計では、よく「これはリソースなのか、操作なのか」という話になる。たとえば POST /orders/search は実務的には自然だ。ただ、search という操作名がパスに入っているぶん、少しRPCっぽくも見える。 一方 QUERY を使えば、QUERY /orders と書ける。対象はあくまで /orders で、その注文リソース群に対して問い合わせをしている。HTTPメソッドとリソースの役割分担としては、こちらの方がきれいに見える。

もちろん、きれいだから常に良いというわけではない。実務では、対応状況やチームの理解の方が大事なことも多い。それでも設計としては、かなり納得感がある。

POST検索は間違いだったのか

では、これまでの POST /search は間違いだったのか。個人的には、そうは思わない。むしろ現実的な制約の中では、かなり妥当な設計だった。

GETではURLが長くなりすぎる。GET with Bodyは実装上の不安がある。複雑な検索条件はJSONで送りたい。となれば、POSTを使うしかなかったのだ。

QUERY は、POST検索を否定するものではない。POST検索が担っていた役割の一部を、HTTPとして正式に表現できるようにしたものと見るのがよい。今まではPOSTで代用していたが、これからは意味としてQUERYの方が合う場面が出てくる。そのくらいの温度感がちょうどいい。 以上、HTTP QUERY メソッドについて調べたことをまとめた。GETは、URLで表現できるシンプルな取得に向いている。POSTは、作成や変更など副作用のある処理に向く。その間には、複雑な検索条件をBodyで送りたいだけの読み取りAPIがあり、これまでは POST /search でなんとかしてきた。QUERY は、その中間に名前と意味を与えるメソッドである。 とはいえ標準化されたからといって、周辺がすぐ追いつくとは限らない。CDN、WAF、API Gateway、フレームワーク、クライアントライブラリといった関所の顔ぶれは、そう簡単には入れ替わらないのだ。現時点での僕の見方はこうである。単純な検索ならGET、複雑な検索で互換性を重んじるならPOST、HTTPの意味を正確に表したいならQUERY。QUERY はいますぐ主流になるものではない。これまでなんとなくやっていたBody付き検索APIに、正直な申告書の書式を用意したものなのだ。

参考