© TOOLS BOX — Next.js / React / TypeScript コードサンプル集

サンプルガイド
←サンプル一覧
nextjsapi

Next.js Route Handler でクエリパラメータを使ってソートを実装する

sort キーと order(asc/desc)をクエリパラメータで受け取り、Route Handler でデータをソートして返すパターンと、クライアント側のソートボタン UI を示す例。

難易度: 初級·更新: 2026-04-18

対応バージョン

nextjs 15react 19

前提環境

Next.js App Router の Route Handler と URLSearchParams の基本を理解していること

概要

GET /api/items?sort=price&order=asc のように sort(キー名)と order(asc / desc)をクエリパラメータで受け取る Route Handler を実装する。許可するソートキーを allowlist で制限して任意フィールドアクセスを防ぎ、クライアント側はソートボタンで URL を更新してデータを再取得する。

インストール

# 追加インストールは不要

実装

Route Handler(ソート付き API)

// app/api/items/route.ts
import { NextResponse } from "next/server";

type Item = {
  id: number;
  name: string;
  price: number;
  createdAt: string;
};

// サンプルデータ(実際のアプリでは DB から取得)
const items: Item[] = [
  { id: 1, name: "りんご", price: 150, createdAt: "2024-01-03" },
  { id: 2, name: "バナナ", price: 80, createdAt: "2024-01-01" },
  { id: 3, name: "オレンジ", price: 120, createdAt: "2024-01-02" },
  { id: 4, name: "ぶどう", price: 300, createdAt: "2024-01-04" },
];

// 許可するソートキーを明示的に制限する
const ALLOWED_SORT_KEYS = ["name", "price", "createdAt"] as const;
type SortKey = (typeof ALLOWED_SORT_KEYS)[number];

function isAllowedSortKey(key: string): key is SortKey {
  return (ALLOWED_SORT_KEYS as readonly string[]).includes(key);
}

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const sortParam = searchParams.get("sort") ?? "id";
  const order = searchParams.get("order") === "desc" ? "desc" : "asc";

  // 不正なキーはデフォルト(id)にフォールバック
  const sortKey = isAllowedSortKey(sortParam) ? sortParam : "name";

  const sorted = [...items].sort((a, b) => {
    const aVal = a[sortKey];
    const bVal = b[sortKey];

    if (typeof aVal === "number" && typeof bVal === "number") {
      return order === "asc" ? aVal - bVal : bVal - aVal;
    }

    const aStr = String(aVal);
    const bStr = String(bVal);
    return order === "asc"
      ? aStr.localeCompare(bStr, "ja")
      : bStr.localeCompare(aStr, "ja");
  });

  return NextResponse.json({ items: sorted, sort: sortKey, order });
}

クライアントコンポーネント(ソートボタン UI)

// components/SortableItemList.tsx
"use client";

import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";

type Item = {
  id: number;
  name: string;
  price: number;
  createdAt: string;
};

type SortKey = "name" | "price" | "createdAt";
type Order = "asc" | "desc";

const SORT_LABELS: Record<SortKey, string> = {
  name: "名前",
  price: "価格",
  createdAt: "登録日",
};

export default function SortableItemList() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const currentSort = (searchParams.get("sort") ?? "name") as SortKey;
  const currentOrder = (searchParams.get("order") ?? "asc") as Order;

  const [items, setItems] = useState<Item[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/items?sort=${currentSort}&order=${currentOrder}`)
      .then((r) => r.json())
      .then((data: { items: Item[] }) => {
        setItems(data.items);
        setLoading(false);
      });
  }, [currentSort, currentOrder]);

  function handleSort(key: SortKey) {
    const nextOrder =
      currentSort === key && currentOrder === "asc" ? "desc" : "asc";
    const params = new URLSearchParams({ sort: key, order: nextOrder });
    router.push(`?${params.toString()}`);
  }

  function SortIcon({ sortKey }: { sortKey: SortKey }) {
    if (currentSort !== sortKey) return <span className="text-gray-300">↕</span>;
    return <span>{currentOrder === "asc" ? "↑" : "↓"}</span>;
  }

  return (
    <div className="overflow-x-auto">
      <table className="w-full text-sm">
        <thead>
          <tr className="border-b bg-gray-50">
            {(Object.keys(SORT_LABELS) as SortKey[]).map((key) => (
              <th key={key} className="px-4 py-3 text-left font-medium">
                <button
                  onClick={() => handleSort(key)}
                  className="flex items-center gap-1 hover:text-blue-600"
                >
                  {SORT_LABELS[key]} <SortIcon sortKey={key} />
                </button>
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {loading ? (
            <tr>
              <td colSpan={3} className="px-4 py-8 text-center text-gray-400">
                読み込み中…
              </td>
            </tr>
          ) : (
            items.map((item) => (
              <tr key={item.id} className="border-b hover:bg-gray-50">
                <td className="px-4 py-3">{item.name}</td>
                <td className="px-4 py-3">¥{item.price.toLocaleString()}</td>
                <td className="px-4 py-3">{item.createdAt}</td>
              </tr>
            ))
          )}
        </tbody>
      </table>
    </div>
  );
}

ページへの組み込み

// app/items/page.tsx
import { Suspense } from "react";
import SortableItemList from "@/components/SortableItemList";

export default function ItemsPage() {
  return (
    <main className="mx-auto max-w-2xl p-8">
      <h1 className="mb-6 text-2xl font-bold">商品一覧</h1>
      <Suspense fallback={<p className="text-gray-400">読み込み中…</p>}>
        <SortableItemList />
      </Suspense>
    </main>
  );
}

ポイント

  • ソートキーを ALLOWED_SORT_KEYS の allowlist で制限し、不正な文字列が渡されたときはデフォルトキーにフォールバックする。ユーザー入力をそのままオブジェクトキーに使うと任意フィールドアクセスやインジェクションリスクがある
  • order パラメータは "desc" のみを明示的に受け入れ、それ以外はすべて "asc" 扱いにすることで不正値を無害化する
  • クライアント側では useSearchParams で現在のソート状態を読み取り、同じキーを再クリックしたときに asc ↔ desc を切り替える。URL に状態を持つため、ブラウザバックや URL 共有でもソートが維持される
  • useEffect の依存配列に currentSort と currentOrder を含めることで、URL パラメータの変化を検知してデータを再取得する
  • Suspense でラップすることで useSearchParams をサーバー側のビルドエラーなしに使用できる(Next.js App Router の要件)

注意点

nextjs-api-search は検索キーワードによるフィルタリング。nextjs-api-pagination はページネーション付き一覧。これは sort キー + asc/desc order の 2 パラメータによるソートに特化。許可キーの allowlist バリデーション・クライアント UI でのソート状態管理も示す。

関連サンプル

同じテーマや技術スタックを使った実装例

  • Next.js Route Handler でサーバーサイドキーワード検索を実装する

    クエリパラメータで受け取ったキーワードを Route Handler でサーバーサイドフィルタし、ページネーション付きで返す例。クライアントのデバウンス入力と合わせた構成も示す。

  • Prisma でフィルタ条件を動的に組み立ててクエリを実行する

    URL クエリパラメータから受け取った検索条件を Prisma の where 句に動的に組み立て、キーワード・カテゴリ・難易度などの複合フィルタクエリを実行するパターン。

  • Prisma で複数カラムをまたぐキーワード検索クエリを実装する

    タイトル・本文・タグなど複数カラムをまたぐキーワード検索を Prisma の OR 条件と fulltext 検索で実装するパターン。単語分割・スコアリングなしの実用的な部分一致パターンを示す。

  • Jest で Next.js Route Handler をテストする

    Request / Response オブジェクトを直接渡して Route Handler 関数をユニットテストする例。GET / POST それぞれのハッピーパスとエラーパスを検証する。

  • Next.js Route Handler でエラーレスポンスを型安全に統一する

    Route Handler のエラーレスポンスを型付きで統一し、400 / 404 / 500 を一貫して扱うラッパー関数を実装する例。try/catch パターンとエラー型の整理も示す。

関連仕様

このサンプルを理解するのに役立つ仕様や概念

  • FrameworkNext.jsReact ベースのフルスタックフレームワーク。SSR・SSG・App Router・API Routes を提供する。
←サンプル一覧に戻る