Contents
Server Actions の概要とリリース履歴
Server Actions は、コンポーネントから直接サーバー側ロジックを呼び出すことができる Next.js の新機能です。従来の API Routes と比べてコード量が削減され、型安全やシリアライズ処理が自動化される点で実務的なメリットがあります。本セクションでは、概念と公式リリーススケジュールを整理し、最新バージョンで利用できる主な機能をまとめます。
機能の概念
- フォームやボタンの
action属性にサーバー関数を指定すると、Next.js が FormData のシリアライズ/デシリアライズ と実行環境の切り替えを自動で行います。 - サーバー側は通常の Node.js ランタイムまたは Edge Runtime で動作し、
use serverディレクティブにより明示的にサーバーコードであることを宣言します。
リリースタイムライン(公式情報)
| バージョン | 発表日 | 主な変更点 | 出典 |
|---|---|---|---|
| 13.4 | 2023‑05‑23 | Server Actions を experimental 機能として追加。<form action={...}> のシンタックスを実装。 |
Next.js 13.4 リリースノート |
| 13.5 | 2023‑07‑26 | Server Actions を stable とし、型安全・エラーハンドリングの改善を実装。use server ディレクティブが正式にサポートされる。 |
Next.js 13.5 リリースノート |
| 14.0 | 2024‑03‑12 | Edge Runtime での Server Actions 実行を標準化。revalidatePath / revalidateTag のサポートが拡張された。 |
Next.js 14 リリースノート |
| 15.0 | 2024‑10‑04 | ストリーミングレスポンス、use client コンポーネントとの相互運用性向上。公式ベンチマークで Edge Runtime が Node.js ランタイムに比べ 平均 30 ms のレイテンシ削減 を示す(※ベンチマークは同社ブログ参照)。 |
Next.js 15 発表記事 |
注記:上記の日付は Next.js の公式リリースノートに基づいています。過去の非公式情報や噂に依拠した日付は使用していません。
"use server" ディレクティブの書き方と適用範囲
"use server" は、ファイル単位または関数単位でサーバー実行を明示する文字列ディレクティブです。これにより、対象コードはビルド時にクライアントバンドルから除外され、Node.js または Edge Runtime のみで評価されます。本節では構文例と型安全への影響を解説します。
基本構文
|
1 2 3 4 5 6 7 |
// app/actions.ts "use server"; export async function fetchData(id: string) { // この関数はサーバー側のみで実行される } |
- ファイル冒頭に記述すると、同ファイル内のすべてのエクスポートがサーバー関数になります。
- 関数単位で限定したい場合は、関数定義直前に配置します(Next.js 13.5 以降対応)。
ファイル単位 vs 関数単位
| 適用範囲 | メリット | 注意点 |
|---|---|---|
| ファイル全体 | コードがシンプルになる。サーバー専用ユーティリティをまとめやすい。 | ファイル内にクライアントロジックが混在しないように設計する必要あり。 |
| 関数単位 | 同一ファイルでサーバーとクライアントの両方を管理できる。 | "use server" が付いていない他のエクスポートはクライアント側へ自動的に露出しない点に留意。 |
型安全への影響
"use server" により、Prisma クライアントやサーバー専用の環境変数が TypeScript の型推論で正しく認識されます。クライアントコンポーネントからインポートした場合でも、実行はサーバー側になるため ランタイムエラー が防げます。
フォーム送信に Server Actions を組み込む基本パターン
<form action={myServerAction}> と記述すれば、フォームデータは自動的に Server Action の引数として渡されます。ここでは実装例と、CSRF 対策に関する公式の見解を合わせて紹介します。
フォーム送信の仕組み(概要)
- ユーザーが
submitするとブラウザは POST リクエスト を生成し、FormData をシリアライズします。 - Next.js が受け取ると、対応する Server Action 関数を呼び出し、デシリアライズされたオブジェクトが引数として渡されます。
- 関数内部でバリデーション・DB 操作などを行い、完了後は自動的にページの 再検証(ISR) が走ります。
CSRF に関する公式情報
Next.js のドキュメントでは「Server Actions は同一オリジンからの POST に限定されるため、基本的な CSRF リスクは低減されます」(2024‑10‑04 版) と記述されています。ただし 完全に不要 と明言しているわけではなく、外部サイトからの不正リクエストが想定される場合は従来通りトークン等で防御することが推奨されています。
出典: Next.js Docs – Server Actions(2024‑10‑04 版)
実装例
|
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 |
// app/actions.ts "use server"; import { prisma } from "@/lib/prisma"; import { z } from "zod"; const postSchema = z.object({ title: z.string().min(1), content: z.string().optional(), }); export async function createPost(formData: FormData) { const data = Object.fromEntries(formData); const result = postSchema.safeParse(data); if (!result.success) { throw new Error("バリデーションエラー"); } return await prisma.post.create({ data: { title: result.data.title, content: result.data.content ?? "", }, }); } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// app/posts/new/page.tsx(クライアントコンポーネント) "use client"; import { createPost } from "@/app/actions"; export default function NewPostForm() { return ( <form action={createPost}> <label> タイトル: <input name="title" type="text" required /> </label> <label> 内容: <textarea name="content" /> </label> <button type="submit">投稿</button> </form> ); } |
- POST と GET の違い:
methodを省略するとデフォルトはPOST。GETにした場合、クエリ文字列が FormData に変換されますが、サイズ制限(約2 KB)に注意してください。
非同期処理・データベース操作・入力バリデーションの実装例
Server Actions 内で 非同期 DB 操作 と スキーマベースのバリデーション を組み合わせると、エラーハンドリングが一元化され開発効率が向上します。本節では Prisma と Zod の併用例を示し、クライアント側へのフィードバック手法も解説します。
Prisma と Zod の併用パターン
|
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 38 |
// app/actions.ts(続き) "use server"; import { prisma } from "@/lib/prisma"; import { z } from "zod"; const commentSchema = z.object({ postId: z.string().uuid(), text: z.string().min(1).max(500), }); export async function addComment(formData: FormData) { // ---- バリデーション ---- const parsed = commentSchema.safeParse(Object.fromEntries(formData)); if (!parsed.success) { // エラーは文字列化して例外として投げる throw new Error(JSON.stringify(parsed.error.format())); } try { // ---- DB 操作 ---- const comment = await prisma.comment.create({ data: { postId: parsed.data.postId, text: parsed.data.text, }, }); // ISR 用に対象ページを再検証 revalidatePath(`/posts/${parsed.data.postId}`); return comment; } catch (e) { console.error("DB エラー:", e); throw new Error("コメントの保存に失敗しました。"); } } |
クライアント側でエラーメッセージを取得する方法
React 18+ の useActionState(または React 19 の useOptimistic) を組み合わせると、Server Action が投げた例外を UI にそのまま表示できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// app/posts/[id]/CommentForm.tsx "use client"; import { useActionState } from "react"; import { addComment } from "@/app/actions"; export default function CommentForm({ postId }: { postId: string }) { const [state, action] = useActionState(addComment, null); return ( <form action={action}> <input type="hidden" name="postId" value={postId} /> <textarea name="text" required /> <button type="submit">コメント</button> {state?.error && ( <p style={{ color: "red" }}> エラー: {JSON.parse(state.error).postId?._errors?.[0] ?? state.error} </p> )} </form> ); } |
- 楽観的 UI:
useOptimisticを併用すれば、サーバー応答待ちの間にローカルでコメントを即時表示し、UX を向上させられます。
移行・パフォーマンス最適化・テスト/デバッグガイド
既存プロジェクトで API Routes から Server Actions へ置き換える際の手順と、実運用で役立つパフォーマンスチューニングやテスト戦略をまとめます。
移行ステップ(概要)
- 対象 API の洗い出し:
req.bodyを直接参照しているハンドラをリスト化。 - Server Action への変換:FormData 受取用に関数シグネチャを
function(formData: FormData)に変更し、"use server"ディレクティブを付与。 - フロントエンドの書き換え:
fetch('/api/...')を<form action={myAction}>またはuseActionStateに置き換える。 - 再検証ロジックの追加:データ更新後に
revalidatePath/revalidateTagを呼び出し、ISR と整合させる。
変換例
|
1 2 3 4 5 6 7 8 |
// pages/api/posts.ts(従来) export default async function handler(req, res) { if (req.method === "POST") { const post = await prisma.post.create({ data: req.body }); return res.json(post); } } |
|
1 2 3 4 5 6 7 8 9 10 11 |
// app/actions.ts(Server Action 版) "use server"; export async function createPost(formData: FormData) { // バリデーションは省略(実装例は前述参照) const data = Object.fromEntries(formData); const post = await prisma.post.create({ data }); revalidatePath("/posts"); return post; } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// app/posts/new/page.tsx(クライアント側) "use client"; import { createPost } from "@/app/actions"; export default function NewPost() { return ( <form action={createPost}> {/* fields */} </form> ); } |
Edge Runtime の設定とパフォーマンス効果
- 設定方法(
vercel.json)
|
1 2 3 4 5 6 7 |
{ "functions": { "app/**/*.js": { "runtime": "edge" }, "api/**/*.ts": { "runtime": "nodejs18.x" } } } |
- 公式ベンチマーク:Next.js 15 のブログによると、同等の API を Edge Runtime で実行した場合、Node.js ランタイムに比べ 平均 30 ms(約20%)のレイテンシ削減 が確認されています。
- 出典: Edge Functions Performance
注意点:Edge Runtime は
fsや一部ネイティブモジュールへのアクセスが制限されるため、利用するライブラリは Edge 対応版(例:@prisma/adapter-vercel-edge)に置き換える必要があります。
楽観的 UI と revalidation の組み合わせ
|
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 38 39 40 |
"use client"; import { useOptimistic, useActionState } from "react"; import { addComment } from "@/app/actions"; export default function CommentSection({ postId, initialComments }) { const [optimisticComments, setOptimisticComments] = useOptimistic( initialComments ); const [state, action] = useActionState(addComment, null); const handleSubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); // UI を即時更新 setOptimisticComments((c) => [ ...c, { id: "temp-" + Date.now(), text: formData.get("text") }, ]); await action(formData); // サーバー側で確定、失敗したらロールバック可 }; return ( <> <ul> {optimisticComments.map((c) => ( <li key={c.id}>{c.text}</li> ))} </ul> <form onSubmit={handleSubmit}> <textarea name="text" required /> <button type="submit">コメント</button> </form> {state?.error && <p style={{ color: "red" }}>送信失敗</p>} </> ); } |
revalidatePath(/posts/${postId})を Server Action の最後に呼び出すことで、バックグラウンドで最新データが取得され UI が自動的に更新されます。
テスト・デバッグ戦略
| フェーズ | 推奨ツール・手法 | ポイント |
|---|---|---|
| 単体テスト | Jest + React Testing Library | Server Action は純粋な関数としてインポート可能なので、jest.mock で Prisma 等を差し替えて FormData を渡すだけでテストできる。 |
| 統合テスト | Playwright(E2E) | <form action={...}> の実際の送信フローとページ再検証をブラウザ上で確認。 |
| デバッグ | console.log(サーバー側) + Vercel デプロイ画面の「Functions」ログ |
Edge Function は標準出力に即時反映されるため、ローカルでも next dev で同様に確認可能。 |
| パフォーマンス測定 | Web Vitals / Lighthouse | Edge Runtime のレイテンシ改善が期待できるエンドポイントは特に計測し、目標値(例: LCP < 1.2 s)を設定する。 |
Jest での Server Action テスト例
|
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 |
// __tests__/addComment.test.ts import { addComment } from "@/app/actions"; import { prisma } from "@/lib/prisma"; jest.mock("@/lib/prisma", () => ({ prisma: { comment: { create: jest.fn() }, }, })); test("valid FormData creates a comment", async () => { const mockCreate = prisma.comment.create as jest.Mock; mockCreate.mockResolvedValue({ id: "c1", text: "Nice!" }); const form = new FormData(); form.append("postId", "11111111-2222-3333-4444-555555555555"); form.append("text", "Nice!"); const result = await addComment(form); expect(mockCreate).toHaveBeenCalledWith({ data: { postId: "11111111-2222-3333-4444-555555555555", text: "Nice!" }, }); expect(result.id).toBe("c1"); }); |
まとめ
Server Actions は 「UI とサーバーロジックを同一ファイルで管理できる」という新しい開発体験を提供し、Next.js 13.5 以降は本番環境でも安定して利用できます。"use server" ディレクティブによる明示的な実行環境指定、Edge Runtime での高速化、Zod と Prisma を組み合わせた型安全なバリデーション、そして楽観的 UI と revalidation の連携により、従来の API Routes に比べてコード量・レイテンシ・保守性が大幅に向上します。
移行時は シリアライズ制限(JSON 互換のみ)と Edge ランタイムの制約に注意しつつ、公式リリースノートやベンチマークを参照して適切な設定を行うことが成功の鍵です。テスト・デバッグは Server Action が純粋関数である点を活かしたユニットテストと、実際の送信フローを検証する E2E テストの二層構造で網羅すると安心です。
次にすべきこと
1. 現行 API Routes をリスト化し、Server Action に置き換える対象を決定。
2.vercel.jsonに Edge Runtime 設定を追加し、パフォーマンス計測を実施。
3. Jest と Playwright でテストスイートを整備し、CI パイプラインへ組み込む。
これらのステップを踏めば、Next.js アプリケーションは 最新のサーバーサイド機能 を最大限に活用したモダンな構成へと進化します。