10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【RFC 10008】16年ぶりの新HTTPメソッド「QUERY」とは?GoとReactで今すぐ実装してみた

10
Posted at

はじめに

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 メソッドです。


成果物

query.gif

今回は、キーワード検索と価格の最小・最大フィルターを同時に適用できる商品検索アプリを作成しました。

ブラウザの「開発者ツール(Networkタブ)」を確認すると、これまで GET や POST だったリクエストが、見事に QUERY メソッドとして発行され、リクエストボディのJSONパラメータに基づいて安全にデータが絞り込まれていることが分かります。


ソースコード

今回はバックエンドに Go (Echo v5)、フロントエンドに React (TypeScript + Fetch API) を採用しました。

Go (バックエンド)

main.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(&params); 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"と文字列を直接定義する必要があります。
image.png

ちなみに、issuesで議論されているため、近い内に定数定義されると思います。

React (フロントエンド)

ブラウザ標準の fetch() APIも、method プロパティには任意の文字列を渡せるため、特別なライブラリのアップデートを待つことなく "QUERY" を指定可能です。POST の時と同じように body に JSON 文字列をセットするだけで、スマートに検索リクエストを発行できます。

App.tsx
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の新しいスタンダードとなっていくはずです。

参考リンク

10
4
1

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
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?