はじめに
HTTPメソッドに「QUERY」という新たなメソッドが公式に追加されました。
複雑な条件を扱う検索・問い合わせに特化した機能のようなので、今回は仕様の背景を追いながら実際にコードを書いて触ってみようと思います。
Queryとは
QUERYメソッドは、2026年6月に発行された RFC 10008 (The HTTP QUERY Method) によって標準化された新しいHTTPメソッドです。汎用的なHTTPメソッドの追加としては、2010年の PATCH(RFC 5789)以来、実に16年ぶりとなります。
仕様の詳細は、以下のIETF公式ドキュメントで公開されています。
これまで、データの検索や問い合わせを行う際は GET メソッドを使い、検索条件(オプションキー)はURLのクエリパラメータに設定するのが基本でした。しかし、実務のWebアプリケーション設計においては、クエリパラメータだけでは対応が難しい以下のようなケースが長年問題になっていました。
- 複雑なデータ構造を送れない: 複数項目の範囲指定やAND/ORが絡む条件、構造化されたJSONパラメータをURLパラメータだけで美しく表現するのが困難。
- URLの長さ制限とエンコードの辛さ: 検索条件が多くなるとURLの長さ制限(ブラウザやプロキシの仕様)に引っかかったり、日本語や記号のパース処理が複雑化する。
- 情報漏洩のリスク: URLにパスワードや個人情報などの敏感な検索キーが含まれていると、サーバーのアクセスログやブラウザの履歴にそのまま残ってしまう。
これらを避けるために「仕方なく POST メソッドでリクエストボディにJSONを入れて検索する」という妥協案がよく使われてきました。しかし、POST は本来「新しいリソースの作成・処理」を意味するメソッドであるため、セマンティクス(意味論)として不自然であり、読み取り専用であることがプロキシやクライアントに伝わらず、HTTPキャッシュも効きにくいという課題がありました。
この「GETとPOSTのいいとこ取り」をして、「リクエストボディに複雑な検索条件を載せて安全に送信できる、読み取り・問い合わせ専用(安全かつべき等)のメソッド」として誕生したのが、今回の QUERY メソッドです。
成果物
今回は、キーワード検索と価格の最小・最大フィルターを同時に適用できる商品検索アプリを作成しました。
ブラウザの「開発者ツール(Networkタブ)」を確認すると、これまで GET や POST だったリクエストが、見事に QUERY メソッドとして発行され、リクエストボディのJSONパラメータに基づいて安全にデータが絞り込まれていることが分かります。
ソースコード
今回はバックエンドに Go (Echo v5)、フロントエンドに React (TypeScript + Fetch API) を採用しました。
Go (バックエンド)
package main
import (
"log"
"net/http"
"strings"
"[github.com/labstack/echo/v5](https://github.com/labstack/echo/v5)"
"[github.com/labstack/echo/v5/middleware](https://github.com/labstack/echo/v5/middleware)"
)
type Item struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price int `json:"price"`
ImageURL string `json:"imageUrl"`
}
var mockItems = []Item{
{ID: 1, Name: "Ergonomic Keyboard", Description: "Mechanical keyboard with brown switches and RGB lighting.", Price: 150, ImageURL: "[https://images.unsplash.com/photo-1595225476474-87563907a212?auto=format&fit=crop&w=400&q=80](https://images.unsplash.com/photo-1595225476474-87563907a212?auto=format&fit=crop&w=400&q=80)"},
{ID: 2, Name: "Wireless Mouse", Description: "Ergonomic wireless mouse with fast scrolling and customizable buttons.", Price: 80, ImageURL: "[https://images.unsplash.com/photo-1527864550417-7fd91fc51a46?auto=format&fit=crop&w=400&q=80](https://images.unsplash.com/photo-1527864550417-7fd91fc51a46?auto=format&fit=crop&w=400&q=80)"},
{ID: 3, Name: "4K Monitor", Description: "27 inch 4K UHD monitor with HDR support and slim bezels.", Price: 350, ImageURL: "[https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?auto=format&fit=crop&w=400&q=80](https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?auto=format&fit=crop&w=400&q=80)"},
{ID: 4, Name: "Noise Cancelling Headphones", Description: "Over-ear headphones with industry-leading active noise cancellation.", Price: 250, ImageURL: "[https://images.unsplash.com/photo-1618366712010-f4ae9c647dcb?auto=format&fit=crop&w=400&q=80](https://images.unsplash.com/photo-1618366712010-f4ae9c647dcb?auto=format&fit=crop&w=400&q=80)"},
{ID: 5, Name: "Standing Desk", Description: "Motorized adjustable standing desk with programmable height settings.", Price: 500, ImageURL: "[https://images.unsplash.com/photo-1595515106969-1ce29566ff1c?auto=format&fit=crop&w=400&q=80](https://images.unsplash.com/photo-1595515106969-1ce29566ff1c?auto=format&fit=crop&w=400&q=80)"},
}
type SearchParams struct {
Keyword string `json:"keyword"`
MinPrice int `json:"minPrice"`
MaxPrice int `json:"maxPrice"`
}
func main() {
e := echo.New()
e.Use(middleware.Recover())
// 【重要】CORSのAllowMethodsに "QUERY" を忘れずに追加
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"http://localhost:3000", "[http://127.0.0.1:3000](http://127.0.0.1:3000)", "http://localhost:5173", "[http://127.0.0.1:5173](http://127.0.0.1:5173)"},
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete, "QUERY"},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
}))
// 独自に "QUERY" メソッドのルーティングを登録
e.Add("QUERY", "/api/search", func(c *echo.Context) error {
var params SearchParams
if err := c.Bind(¶ms); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
}
log.Printf("Received QUERY request with params: %+v\n", params)
var results []Item
for _, item := range mockItems {
match := true
if params.Keyword != "" {
if !strings.Contains(strings.ToLower(item.Name), strings.ToLower(params.Keyword)) &&
!strings.Contains(strings.ToLower(item.Description), strings.ToLower(params.Keyword)) {
match = false
}
}
if params.MinPrice > 0 && item.Price < params.MinPrice {
match = false
}
if params.MaxPrice > 0 && item.Price > params.MaxPrice {
match = false
}
if match {
results = append(results, item)
}
}
return c.JSON(http.StatusOK, results)
})
log.Fatal(e.Start(":8080"))
}
AllowMethods: []string{"QUERY",http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
許容するメソッド定義追加時に、Goではまだ定数が用意されていません。
そのため、"QUERY"と文字列を直接定義する必要があります。

ちなみに、issuesで議論されているため、近い内に定数定義されると思います。
React (フロントエンド)
ブラウザ標準の fetch() APIも、method プロパティには任意の文字列を渡せるため、特別なライブラリのアップデートを待つことなく "QUERY" を指定可能です。POST の時と同じように body に JSON 文字列をセットするだけで、スマートに検索リクエストを発行できます。
import React, { useState, useEffect, FormEvent } from 'react';
import './App.css';
interface Item {
id: number;
name: string;
description: string;
price: number;
imageUrl: string;
}
function App() {
const [items, setItems] = useState<Item[]>([]);
const [keyword, setKeyword] = useState('');
const [minPrice, setMinPrice] = useState('');
const [maxPrice, setMaxPrice] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchItems = async (searchParams: any) => {
setLoading(true);
setError(null);
try {
// 新規追加された QUERY メソッドを指定し、ボディに検索JSONを載せる
const response = await fetch('http://localhost:8080/api/search', {
method: 'QUERY',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(searchParams),
});
if (!response.ok) {
throw new Error(`Error: ${response.status}${response.statusText}`);
}
const data = await response.json();
setItems(data || []);
} catch (err: any) {
setError(err.message || 'An error occurred while fetching data.');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchItems({});
}, []);
const handleSearch = (e: FormEvent) => {
e.preventDefault();
fetchItems({
keyword,
minPrice: minPrice ? parseInt(minPrice, 10) : 0,
maxPrice: maxPrice ? parseInt(maxPrice, 10) : 0,
});
};
return (
<div className="container">
<header className="header">
<h1>TechGadget Explorer</h1>
<p className="subtitle">Discover premium gear using the new REST QUERY method</p>
</header>
<section className="search-section">
<form onSubmit={handleSearch} className="search-form">
<div className="input-group">
<label htmlFor="keyword">Keyword</label>
<input
type="text"
id="keyword"
placeholder="Search products..."
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
</div>
<div className="input-group">
<label htmlFor="minPrice">Min Price ($)</label>
<input
type="number"
id="minPrice"
placeholder="e.g. 50"
min="0"
value={minPrice}
onChange={(e) => setMinPrice(e.target.value)}
/>
</div>
<div className="input-group">
<label htmlFor="maxPrice">Max Price ($)</label>
<input
type="number"
id="maxPrice"
placeholder="e.g. 500"
min="0"
value={maxPrice}
onChange={(e) => setMaxPrice(e.target.value)}
/>
</div>
<button type="submit" className="search-button">
Search
</button>
</form>
</section>
{loading && <div className="loading">Searching the catalog...</div>}
{error && <div className="error">{error}</div>}
{!loading && !error && (
<section className="results-section">
{items.length === 0 ? (
<div className="empty-state">
<h3>No products found</h3>
<p>Try adjusting your search filters.</p>
</div>
) : (
items.map((item) => (
<div key={item.id} className="item-card">
<img src={item.imageUrl} alt={item.name} className="item-image" />
<div className="item-content">
<h2 className="item-title">{item.name}</h2>
<p className="item-desc">{item.description}</p>
<div className="item-footer">
<span className="item-price">${item.price}</span>
</div>
</div>
</div>
))
)}
</section>
)}
</div>
);
}
export default App;
まとめ
現時点ではまだ新しい仕様であるため、本番環境で導入する際にはAWSのALBやCloudflareなどのCDN、各種プロキシやWAFが未知のHTTPメソッドを弾かずに正しくルーティングできるか検証が必要です。しかし、「検索条件が複雑だから」という理由だけで不自然な POST を採用する必要がなくなることは、REST API設計における大きな進歩と言えます。今後各クライアントやミドルウェアの対応が進むにつれ、検索・問い合わせAPIの新しいスタンダードとなっていくはずです。
