👊

あなたは大丈夫?Next.jsで押さえておきたい“Server Actions”と“Route Handlers”を使い分けるコツ

に公開

TL;DR

  • Server Actions (SA): UI 直結の POST‑専用 RPC。フォームやボタンから直接呼び出し、ミューテーションに最適。
  • Route Handlers (RH): 従来型 REST/HTTP API。GET でのデータ取得や外部クライアント連携、Webhook 受信などに強い。
  • 迷ったら 「読む → GET → RH」「書く → POST → SA」 のワンルールで OK!

1. なぜ “二本柱” なのか

Next.js 14 から React Server Components (RSC) が本格採用され、
フロントとバックエンドの垣根がさらに低くなりました。
その中核が Server ActionsRoute Handlers の 2 つです。

Server Actions Route Handlers
呼び出し方法 <form action={fn}>onClick={fn} fetch("/api/*")/外部 HTTP
対応 HTTP POST 固定 GET / POST / PUT / PATCH / DELETE
主な用途 UI 専用 ミューテーション データ取得/外部向け API/Webhook
キャッシュ制御 revalidatePath() など RSC キャッシュ連動 Cache‑Control ヘッダー
型共有 フロントとサーバーで TypeScript をそのまま共有 可能(fetch レイヤが必要)

“役割分担” が明確になると、フルスタック開発の設計が一気にシンプルになります。


2. Server Actions — UI 直結の RPC スタイル

特徴

  • UI から直接呼び出し
    コンポーネント内で use server を付けるだけで RPC エンドポイント化。
  • 常に POSTミューテーション専任 が意図的に強制される。
  • 型を丸ごと共有 — 関数を import すればフロントとサーバーが同じ型を参照。
  • RSC キャッシュと連動revalidatePath() / revalidateTag() で即時再取得。
"use client";
import { revalidatePath } from "next/cache";

export default function Page() {
  async function addTodo(formData: FormData) {
    "use server";                       // ← これだけで RPC
    const title = formData.get("title") as string;
    await db.todo.create({ data: { title } });
    revalidatePath("/todos");           // RSC キャッシュを更新
  }

  return (
    <form action={addTodo} className="space-x-2">
      <input name="title" placeholder="買い物メモ" />
      <button type="submit">追加</button>
    </form>
  );
}

制約 & ベストプラクティス

  • 読み取りには不向き — キャッシュが stale‑while‑revalidate 相当で弱い。
  • UI 直結ミューテーション専任 — “フォーム → DB 書き込み → 画面更新” に特化。

3. Route Handlers — 柔軟な汎用 API 層

特徴

  • ファイルベースで直感的
    app/api/**/route.ts に各 HTTP メソッドを並べるだけ。
  • HTTP キャッシュを細かく制御
    Cache-Control, ETag, SWR と併用し高速な GET を実現。
  • Webhook/外部クライアント対応
    Stripe Webhook, 社内 CLI など外部との境界線を担当。
  • 認証・CORS にも一括対応
    ミドルウェアやヘッダーで柔軟に設定可能。
// app/api/todos/route.ts
import { NextResponse } from "next/server";

export async function GET() {
  const todos = await db.todo.findMany();
  return NextResponse.json(todos, {
    headers: { "Cache-Control": "s-maxage=60, stale-while-revalidate=120" },
  });
}

export async function POST(request: Request) {
  const { title } = await request.json();
  const todo = await db.todo.create({ data: { title } });
  return NextResponse.json(todo, { status: 201 });
}

4. 設計指針まとめ — “4 行ルール”

  1. 読む(GET)→ Route Handler
  2. 書く(UI から POST)→ Server Action
  3. 外部/複数クライアントと共有Route Handler
  4. UI 専用ミューテーションServer Action

5. “無ければ作る” Upsert パターン

Route Handler × Supabase Cron

// app/api/jobs/sync/route.ts
import { NextResponse } from "next/server";
import { supabase } from "@/lib/supabase";

export async function POST() {
  await supabase
    .from("items")
    .upsert({ id: 123, value: "foo" })
    .select();                      // ← 1 往復で insert or update
  return NextResponse.json({ success: true });
}

UI ボタン → Server Action

"use client";
import { revalidatePath } from "next/cache";

export default function SyncButton() {
  async function syncItem() {
    "use server";
    await db.item.upsert({
      where: { id: 123 },
      create: { value: "foo" },
      update: { value: "foo" },
    });
    revalidatePath("/items");
  }

  return <button onClick={syncItem}>同期</button>;
}

6. Cron & 定期ジョブ運用

プラットフォーム 叩く先 代表例
Vercel Cron app/api/*/route.ts 夜間バッチ、RSS 取り込み
Supabase Cron 同上 集計テーブル更新
Edge Scheduler 同上 キャッシュ暖気、Slack 通知

権限制御Service‑role キー + Row Level Security を併用し、
「Cron しか叩けない」最小権限を付与するのが鉄板。


7. 覚え方のコツ

GET は RH、POST は SA、
外から来るなら RH、画面限定なら SA


8. まとめ

  1. Server Actions = UI 直結ミューテーション専任
  2. Route Handlers = 汎用 HTTP API & 外部共有に最適
  3. 「読む→GET→RH」「書く→POST→SA」 を徹底すると設計が激シンプル。
  4. Cron や Edge Function も同じ Route Handler を叩けば、一元管理 & 再利用性アップ。

Next.js の “二本柱” をマスターし、複雑さゼロで強力なフルスタックアプリ を構築しましょう!🎉

Discussion