Contents
Server Actions の全体像とバージョン変遷
Next.js 13 系で初めてプレビューとして提供された Server Actions は、フロントエンドからサーバー側の非同期関数を直接呼び出すことで API Routes を不要にし、開発効率とコードベースのシンプルさを同時に実現します。2024 年に正式リリースされた Next.js 14 では、型安全性や Edge Runtime との互換性が大幅に改善され、プロダクション環境でも安心して利用できるようになりました。本節ではバージョンごとの差分と、公式ドキュメントの最新版 URL を紹介します。
バージョン別主な変更点
以下の表は、実験的機能から安定版へ移行した際に導入された代表的な機能をまとめたものです。表を見るだけで、どのバージョンで何が改善されたか一目で把握できます。
| バージョン | 主な変更点 |
|---|---|
| v13.4 (experimental) | use server ディレクティブが使用可能だが、ビルドサイズ削減は限定的。型推論や Edge 互換性に課題あり。 |
| v14 (stable) | 型推論の強化・Edge Function への自動デプロイ・revalidatePath 等キャッシュ制御 API の標準装備。コンパイラレベルで use server を検出し、サーバー専用関数を確実に分離。 |
参考: 最新の公式ドキュメントは https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations にあります。
正式リリースがもたらす価値
v14 のリリースにより、Server Actions は「実験的」から「本番で推奨できる成熟機能」へと位置付けが変わりました。これにより以下の効果が期待できます。
- 開発速度の向上:API Routes を書く手間が不要になり、フロントエンド側だけで完結したデータ更新が可能になる。
- セキュリティの向上:サーバー専用関数はクライアントに送られないため、機密情報が漏洩するリスクを根本的に排除できる。
- パフォーマンス改善:Edge Runtime への自動デプロイとキャッシュ制御 API により、レイテンシが低減しスケーラビリティが向上する。
基本構文と use server ディレクティブ
Server Action を正しく定義するには、関数の最上部に "use server" と記述してコンパイラに「サーバー側でのみ実行すべき」ことを明示します。このディレクティブが欠けていると、コードはクライアントバンドルに混入し期待通りに動作しません。
ディレクティブの書き方と必須位置
"use server" は 関数宣言の直後 に置く必要があります。以下は TypeScript で典型的な実装例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// app/actions.ts import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; export async function createPost(data: { title: string; content: string }) { "use server"; // ← 必須ディレクティブ const post = await prisma.post.create({ data }); // 作成後にキャッシュを再検証 revalidatePath("/posts"); return post; } |
- ポイント
- ファイル全体でサーバー専用関数だけをエクスポートする場合でも、各関数ごとにディレクティブを書きます。
export const deleteUser = async (id: string) => { "use server"; … }のようなアロー関数でも同様です。
注意点とベストプラクティス
- インラインコメントは避ける:ディレクティブ行の前後に余計なコードやコメントが入らないようにします。
- 型安全性を活かす:Server Action の引数・戻り値は普通の関数と同様に TypeScript で型付けでき、コンパイル時にエラーが検出されます。
- 副作用はサーバー側だけで完結させる:DB 接続や外部 API 呼び出しは必ず
use serverが付いた関数内で行い、クライアントへ直接露出させないようにします。
フォームコンポーネントでの活用例とデータフロー
Server Actions は <form> の action 属性 に直接関数を渡すだけで、POST/GET リクエストをサーバー側関数へマッピングできます。これにより API Routes を経由しないシンプルなデータ送受信が実現します。
POST フォームの実装例
以下は記事投稿用のフォームです。action={createPost} と書くだけで、送信されたデータはサーバー側 createPost 関数へ渡ります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// app/components/NewPostForm.tsx "use client"; import { createPost } from "@/app/actions"; export default function NewPostForm() { return ( <form action={createPost} method="POST" encType="multipart/form-data"> <label> タイトル: <input type="text" name="title" required /> </label> <label> 本文: <textarea name="content" required></textarea> </label> <button type="submit">投稿する</button> </form> ); } |
- ポイント
method="POST"とencType="multipart/form-data"を指定すれば、ファイル添付や大容量データもそのままサーバー関数に届く。- CSRF トークンは通常の hidden フィールドで埋め込むだけで、Server Action 側で検証可能です。
GET 検索フォームの実装例
検索系の UI は GET リクエストが自然です。以下はキーワード検索用のシンプルなフォームです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// app/components/SearchForm.tsx "use client"; import { searchPosts } from "@/app/actions"; export default function SearchForm() { return ( <form action={searchPosts} method="GET"> <input type="text" name="q" placeholder="キーワード検索" /> <button type="submit">検索</button> </form> ); } |
- ポイント
- GET の場合はクエリ文字列が URL に残るため、ブックマークやシェアが容易になる。
- サーバー側で
searchPostsが受け取るのはFormDataオブジェクトなので、form.get("q")で簡単に取得できる。
フォーム活用の総合的なメリット
- コード量削減:fetch 呼び出しやハンドラ作成が不要になるため、フロントエンド側の実装が数行に収まります。
- 安全性向上:サーバー関数は常に
use serverが付与された環境で走るため、機密情報がクライアントへ漏れません。 - UX の最適化:
useTransitionと組み合わせれば楽観的 UI 更新も可能です。
Server Components と Client Components の連携・制約
Next.js では Server Component がサーバー側でレンダリングされ、クライアントには HTML と最小限の JavaScript だけが送られます。一方 Client Component はブラウザ上で実行され、状態管理や副作用フック (useState, useEffect) を利用できます。Server Actions を使う際はこの境界を正しく理解しておくことが重要です。
役割と呼び出しパターン
| 呼び出し元 | 推奨手法 | 補足 |
|---|---|---|
| Server Component → Server Action | <form action={...}> のみ使用 |
コンポーネント自体は非同期でないため、直接 await できない。 |
| Client Component → Server Action | フォームの action 属性か useTransition + 関数呼び出し |
UI は即時に更新可能で、バックエンド処理は非同期に走る。 |
| Server Component ← Server Action の結果 | Props 経由または revalidatePath で再フェッチ |
キャッシュが自動的に無効化され、最新データが反映される。 |
- 注意点:Server Component 内に
useStateやuseEffectといったクライアント専用フックを入れないこと。これらはコンパイルエラーの原因になります。
データ受け渡しの制限とベストプラクティス
- FormData のみが安全:Server Action に渡すデータは
FormDataまたはシリアライズ可能な JSON である必要があります。 - props 経由の更新:Server Action が完了したら
revalidatePathを呼び出し、対象ページを再レンダリングさせると UI が自動的に最新になる。 - キャッシュ戦略:頻繁に変わらないデータは
cache: "force-no-store"で無効化し、逆に静的データはrevalidateオプションで期限切れを設定できます。
実務導入ガイド:ミューテーション・パフォーマンス・セキュリティ・デバッグ
実際のプロダクトへ Server Actions を組み込む際に留意すべきポイントは、DB ミューテーションの正しい書き方、Edge デプロイ時のビルドサイズ最適化、CSRF 対策、そして効果的なデバッグ手法です。ここではそれぞれの具体例と推奨設定を示します。
DB ミューテーションと外部 API 呼び出しの実装例
以下は Prisma を用いたユーザープロフィール更新に加えて、Slack Webhook へ通知するパターンです。トランザクションやバリデーションも併せて示しています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// app/actions.ts import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; import { z } from "zod"; const profileSchema = z.object({ id: z.string().uuid(), name: z.string().min(1), avatarUrl: z.string().url().optional(), }); export async function updateUserProfile(form: FormData) { "use server"; // --- バリデーション --- const raw = Object.fromEntries(form.entries()); const parsed = profileSchema.safeParse(raw); if (!parsed.success) throw new Error("バリデーションエラー"); const { id, name, avatarUrl } = parsed.data; // --- DB トランザクション + 外部通知 --- await prisma.$transaction(async (tx) => { await tx.user.update({ where: { id }, data: { name, avatarUrl } }); await fetch(process.env.SLACK_WEBHOOK_URL!, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: `User ${name} updated.` }), }); }); // --- キャッシュ再検証 --- revalidatePath(`/users/${id}`); return { success: true }; } |
- ポイント
- Zod によるスキーマバリデーションで型安全性と入力チェックを一元化。
prisma.$transactionを使うことで DB 更新と外部通知の原子性を確保。
Vercel デプロイ時の最適化設定とビルドサイズ削減テクニック
Edge Runtime にデプロイする場合は、runtime: "edge" の明示と不要な依存の除去が鍵です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// next.config.mjs export default { experimental: { serverActions: true }, // v14 はデフォルトだが明示的に記載 output: "standalone", // Edge Function 用設定例 async redirects() { return [ { source: "/api/:path*", destination: "/api/:path*", permanent: false, }, ]; }, }; |
ビルドサイズ削減のヒント
next/bundle-analyzerを有効化し、どのモジュールがクライアントバンドルに含まれているか可視化。- 大きなサーバー専用パッケージ(例:
sharp,pg)はserverOnly: trueフラグで除外。 - lodash 系は個別インポート (
import debounce from "lodash/debounce") に切り替える。
CSRF 対策と SameSite 設定の実装
Server Action はブラウザから直接呼び出されるため、SameSite=Strict の HttpOnly クッキーで CSRF トークンを管理することが推奨されます。以下はトークン生成・埋め込み・検証までの一連フローです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// app/actions.ts (CSRF 発行) import { cookies } from "next/headers"; export async function getCsrfToken() { "use server"; const token = crypto.randomUUID(); // SameSite を strict に設定し、HttpOnly にすることでクライアント側からの直接取得を防止 cookies().set("csrf-token", token, { httpOnly: true, sameSite: "strict", path: "/", secure: process.env.NODE_ENV === "production", }); return token; } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// app/components/ProtectedForm.tsx (クライアント側) "use client"; import { useEffect, useState } from "react"; export default function ProtectedForm() { const [csrf, setCsrf] = useState(""); useEffect(() => { fetch("/api/get-csrf-token") .then((r) => r.text()) .then(setCsrf); }, []); return ( <form action={submitSensitive} method="POST"> <input type="hidden" name="csrfToken" value={csrf} /> {/* 他の入力フィールド */} <button type="submit">送信</button> </form> ); } |
|
1 2 3 4 5 6 7 8 9 10 |
// app/actions.ts (検証ロジック) export async function submitSensitive(form: FormData) { "use server"; const token = form.get("csrfToken"); if (token !== cookies().get("csrf-token")?.value) { throw new Error("CSRF validation failed"); } // 正常処理へ続く… } |
- ポイント:
cookies().setのオプションでsameSite: "strict"とhttpOnly: trueを必ず指定し、クロスサイトからの送信を防ぎます。
エラーハンドリングとバリデーションのベストプラクティス
- 例外は React の Error Boundary へ:Server Action 内で
throw new Error()すると、対応する Server Component のエラーバウンダリが捕捉できる。 useActionState(v14)で UI にフィードバック:サーバー側の例外メッセージをクライアントに返し、ユーザーへ適切なエラー表示を行う。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// client component example "use client"; import { useActionState } from "react"; export default function DeleteButton({ postId }: { postId: string }) { const [state, formAction] = useActionState(deletePost, null); return ( <> <form action={formAction}> <input type="hidden" name="id" value={postId} /> <button type="submit">削除</button> </form> {state?.error && <p className="text-red-600">{state.error}</p>} </> ); } |
効率的なデバッグ手法
Server Actions のトラブルシューティングは「クライアント側のリクエスト」+「サーバー側の実行ログ」の二方向からアプローチします。
| ツール | 主な用途 |
|---|---|
| React DevTools(Server Components タブ) | Server Component のレンダリング階層と props を確認。use server 関数は表示されないが、呼び出し元の情報が取得可能。 |
| Vercel Dashboard → Functions > Logs | Edge/Node Function の実行ログをリアルタイムで閲覧。console.log がそのまま出力されるので、データ受け渡しやエラー詳細がすぐ分かる。 |
ローカル next dev コンソール |
開発環境では標準出力にスタックトレースが表示され、Node デバッガ (node --inspect) に接続できる。 |
| ブラウザ DevTools → Network タブ | フォーム送信時の HTTP ヘッダー・ペイロードを確認し、CSRF トークンや multipart データが正しく構成されているか検証できる。 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// デバッグ用 console.log の例(削除は忘れずに!) export async function deletePost(form: FormData) { "use server"; console.log("deletePost called:", Object.fromEntries(form.entries())); try { const id = form.get("id") as string; await prisma.post.delete({ where: { id } }); revalidatePath("/posts"); return { success: true }; } catch (e) { console.error("Deletion error:", e); throw new Error("削除に失敗しました"); } } |
- ポイント:
console.logは Vercel の「Functions > Logs」でも確認できるため、ローカルと本番環境で同一のデバッグ手順が使える。エラーは必ずthrowして React の Error Boundary に委ねると、UI 上で統一的にハンドリングできる。
まとめ
Server Actions は フロントエンドからサーバー側ロジックへの最短ルート を提供し、API Routes の煩雑さを解消します。"use server" ディレクティブで安全性を担保しつつ、フォームの action 属性や useTransition と組み合わせることで UX も向上。実務導入時は 型バリデーション・トランザクション・CSRF 対策・ビルドサイズ最適化 を意識し、React DevTools + Vercel Logs による二重デバッグで問題を早期に切り分けましょう。これらのベストプラクティスを守れば、Next.js 14 の Server Actions を本番プロダクトでも自信を持って活用できます。