👊
あなたは大丈夫?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 Actions と Route 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 行ルール”
- 読む(GET)→ Route Handler
- 書く(UI から POST)→ Server Action
- 外部/複数クライアントと共有 → Route Handler
- 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. まとめ
- Server Actions = UI 直結ミューテーション専任
- Route Handlers = 汎用 HTTP API & 外部共有に最適
- 「読む→GET→RH」「書く→POST→SA」 を徹底すると設計が激シンプル。
- Cron や Edge Function も同じ Route Handler を叩けば、一元管理 & 再利用性アップ。
Next.js の “二本柱” をマスターし、複雑さゼロで強力なフルスタックアプリ を構築しましょう!🎉
Discussion