
Next.jsを使った最新のフルスタック開発では、クライアントとサーバーの境界は意図的に曖昧にされています。この融合は開発者の生産性を飛躍的に向上させますが、同時に重要なアーキテクチャ上の問いを投げかけます。サーバーサイドロジックをどこで実行すべきか、という問いです。具体的には、ミューテーション(データの変更)やデータフェッチにおいて、従来のRoute HandlerとServer Actionのどちらを選ぶべきでしょうか?これは好みの問題ではなく、パフォーマンス、セキュリティ、ユーザーエクスペリエンスに深く影響する根本的なアーキテクチャ上の決定です。誤った選択は、プログレッシブエンハンスメントの欠如、キャッシングの問題、セキュリティの脆弱性を抱える脆いアプリケーションにつながる可能性があります。この記事では、シニアエンジニアが常に正しい判断を下すために必要な、深く権威ある分析を提供します。
最も高いレベルでは、この区別は通信パラダイムの違いにあります。
Server Actionsは**Remote Procedure Call (RPC)**モデルを実装しています。クライアントサイドのコードは、サーバー上で実行される関数を呼び出します。ネットワーク境界、HTTPメソッド、データシリアライゼーションは、Next.jsフレームワークによってほとんど抽象化されています。インタラクションの主要な単位は関数シグネチャです。
Route Handlersは**Representational State Transfer (REST)**またはRESTライクなモデルを実装しています。クライアントが標準的なHTTPセマンティクスを使用してインタラクトする、個別のHTTPエンドポイント(GET、POST、PUT、DELETEなど)を作成します。ネットワークは明示的です。インタラクションの主要な単位はURLとHTTPメソッドです。
この根本的な違いが、キャッシングの挙動からセキュリティ体制まで、その後のすべてを決定します。それぞれを詳しく解説します。
Server Actionsは、"use server"ディレクティブでマークされた、サーバー上で実行される非同期関数です。Server Components内で定義することも、別のファイルで定義してClient Componentsにインポートすることもできます。その設計目標は、特にフォーム内でのクライアントからのデータミューテーションを簡素化することです。
Server Actionsの最も魅力的な機能は、プログレッシブエンハンスメントの組み込みサポートです。シンプルなフォームを考えてみましょう。
// app/actions/create-post.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
// ... database logic to create the post ...
console.log(`Created post: ${title}`);
revalidatePath("/"); // Invalidate cache for the home page
return { success: true, title };
}
// app/page.tsx
import { createPost } from "./actions/create-post";
export default function HomePage() {
return (
<main>
<h1>My Blog</h1>
<form action={createPost}>
<input type="text" name="title" required />
<button type="submit">Create Post</button>
</form>
{/* ... list of posts ... */}
</main>
);
}
内部の仕組み:
POSTリクエストを送信します。Next.jsサーバーはこれを受け取り、ターゲットのServer Actionを特定して実行し、その後ページを再レンダリングします。これは古典的で堅牢なウェブの挙動です。fetch POSTリクエストを行います。このリクエストのボディはJSONではなく、FormDataです。Server Actionの一意のID(ビルド時に生成されるハッシュ)は、URLまたは特別なNext-Action HTTPヘッダーに含まれてリクエストに送られます。サーバーはこのリクエストを正しい関数にルーティングし、実行し、戻り値がクライアントにストリーミングされます。Reactはこのデータを使用して、ページ全体のナビゲーションなしでUIを更新します。ページ全体の再読み込みからクライアントサイドのナビゲーションへのこのシームレスなアップグレードは、プログレッシブエンハンスメントの本質であり、DX/UXにおける大きな勝利です。
Client ComponentからServer Actionを呼び出すとき、Next.jsはあるトリックを実行します。
// app/components/UpdateUserButton.tsx
"use client";
import { updateUserEmail } from "@/app/actions/user-actions";
import { useTransition } from "react";
export function UpdateUserButton({ userId }: { userId: string }) {
const [isPending, startTransition] = useTransition();
const handleUpdate = () => {
const newEmail = prompt("Enter new email:");
if (newEmail) {
// The `userId` is "captured" from the component's props.
startTransition(() => updateUserEmail(userId, newEmail));
}
};
return (
<button onClick={handleUpdate} disabled={isPending}>
{isPending ? "Updating..." : "Update Email"}
</button>
);
}
ここで、updateUserEmailはuserIdを必要とするServer Actionです。あなたはフォーム入力からuserIdを渡しているわけではありません。ここで「クロージャのシリアライゼーション」が登場します。updateUserEmail関数は、呼び出し時にuserId値とバインドされます。アクションが呼び出されると、Next.jsはこのバインドされた値をシリアライズし、他の引数とともにサーバーに送信します。
これは真のレキシカルクロージャではありません。 シリアライズ可能な引数のみが「クローズオーバー」できます。関数、Symbol、クラスインスタンスをキャプチャすることはできません。これは、サーバー関数の部分的適用を一時的に作成するためのビルド時およびランタイムメカニズムです。
Server ActionsはNext.jsのキャッシング層と深く統合されています。キャッシュ無効化の主要なメカニズムはプログラムによるものです。
revalidatePath(path):特定のパス上のすべてのfetchリクエストのデータキャッシュを無効にします。これにより、そのパス上のServer Componentsは次回の訪問時に再レンダリングされます。revalidateTag(tag):特定の文字列でタグ付けされたすべてのfetchリクエストのキャッシュを無効にします。これは、アプリケーションの異なる部分にわたるデータを無効化するためにより粒度が高く強力です。このモデルはプッシュベースです。ミューテーション(Server Action)が、どのデータが古くなったかをNext.jsに明示的に伝える責任を負います。
これは、しばしば見過ごされがちな重要な利点です。Server Actionsには、組み込みのクロスサイトリクエストフォージェリ(CSRF)保護があります。
Server Actionが定義されると、Next.jsは一意の、アクションごとのIDを埋め込みます。クライアントから呼び出されると、このIDはNext-Action HTTPヘッダーで送信されます。サーバーでは、受信リクエストのアクションIDがサーバーの既知のアクションIDと照合されます。さらに、フレームワークは、リクエストが自サイトから発信されたことを保証するために、トークンベースの戦略(ダブルサブミットクッキーパターンと比較されることが多いが、内部で管理される)を採用しています。これは自動的に行われ、開発者による設定は不要で、ミューテーションに対してデフォルトで安全な状態を提供します。
Route Handlersは、Pages RouterのAPI Routesの精神的後継です。これらはapp/api/.../route.tsファイルに存在し、標準のWeb RequestおよびResponse APIを活用することで、HTTPリクエストとレスポンスを完全に低レベルで制御できます。
Route Handlerは、HTTPメソッド(GET、POST、PUTなど)にちなんで名付けられた関数をエクスポートすることで定義されます。
// app/api/posts/route.ts
import { NextResponse } from "next/server";
export async function GET(request: Request) {
// Access search params
const { searchParams } = new URL(request.url);
const query = searchParams.get("query");
// ... database logic to fetch posts matching query ...
const posts = [{ id: 1, title: "Hello World" }];
return NextResponse.json(posts);
}
export async function POST(request: Request) {
const body = await request.json();
// ... database logic to create a post ...
const newPost = { id: 2, ...body };
return NextResponse.json(newPost, { status: 201 });
}
これは標準的なRESTエンドポイントです。HTTPを話せるクライアントであれば、ウェブブラウザ、モバイルアプリ、他のバックエンドサービス、またはWebhookプロバイダーなど、どんなクライアントでもこれと対話できます。あなたは以下を直接制御できます。
200、201、400、404、500など。Content-Type、Cache-Control、Location、CORSヘッダーの設定。FormDataの読み書き、さらにはデータのストリーミング。Route Handlersでのキャッシングは明示的であり、HTTP標準に従います。以下を通じて制御します。
// Statically evaluate at build time
export const dynamic = 'force-static';
// Always dynamically evaluate
export const dynamic = 'force-dynamic';
Cache-Controlヘッダー: Responseオブジェクトにキャッシングヘッダーを手動で設定します。これにより、ダウンストリームのキャッシュ(ブラウザ、CDN)の動作を指示します。
return new Response('This will be cached for 60 seconds', {
status: 200,
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=30',
},
});
fetch Route Handlers内のGETリクエストは、Server Componentsと同様に、デフォルトでNext.jsによって自動的にキャッシュされます。{ cache: 'no-store' }を使用するか、{ next: { revalidate: 3600 } }で再検証することで、これをオプトアウトできます。このモデルはプルベースです。データのコンシューマは、エンドポイントが提供するキャッシュディレクティブを尊重する責任があります。
Route Handlersは、AI生成レスポンスや長時間実行されるタスクで一般的なパターンである、データストリーミングに最適です。これはReadableStreamを使用して実現されます。
// app/api/chat/route.ts
import { OpenAIStream, StreamingTextResponse } from 'ai';
import OpenAI from 'openai';
export const runtime = 'edge'; // Edge runtime is great for streaming
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function POST(req: Request) {
const { messages } = await req.json();
const response = await openai.chat.completions.create({
model: 'gpt-4',
stream: true,
messages,
});
const stream = OpenAIStream(response);
return new StreamingTextResponse(stream);
}
このレベルの低レベルなレスポンス操作は、より構造化されたRPCスタイルの戻り値契約を持つServer Actionsでは不可能です。
Server Actionsとは異なり、Route Handlersはセキュリティの負担を開発者に全面的に負わせます。
POST、PUT、またはDELETEハンドラーが機密性の高い操作を実行し、セッションベースである場合、CSRF対策(例:ダブルサブミットクッキー、シンクロナイザートークンパターン)を実装する責任があります。Authorization: Bearer ...)、クッキー、またはその他のトークンを手動で検査する必要があります。視覚的な比較により、異なる実行パスが明確になります。
| 機能 | Server Actions | Route Handlers |
|---|---|---|
| パラダイム | RPC (Remote Procedure Call) | REST (またはRESTライクなHTTPエンドポイント) |
| 主なユースケース | UIに紐づくミューテーション、フォーム処理 | 公開API、Webhook、AJAXソース |
| プログレッシブエンハンスメント | 自動 & 組み込み | 手動 (別途ロジックが必要) |
| セキュリティ (CSRF) | 組み込み、自動保護 | 手動、開発者の責任 |
| セキュリティ (CORS) | 該当なし (同一オリジンのみ) | 手動、ヘッダー設定が必要 |
| 状態管理 | Reactと統合 (useFormStatus, useOptimistic) | 手動 (例: useState, SWR, React Query) |
| キャッシュ無効化 | プログラムによる (revalidatePath, revalidateTag) | クライアント側のfetchタグまたはHTTPヘッダー経由 |
| キャッシュ戦略 | プッシュベース (ミューテーションがキャッシュを無効化) | プルベース (クライアントがCache-Controlを尊重) |
| リクエスト/レスポンス | 関数シグネチャと戻り値によって抽象化 | Web Request/Response APIによる完全な制御 |
| ストリーミング | なし (レスポンスボディの場合) | あり、ReadableStream経由 |
| アクセシビリティ | Next.jsクライアントのみ | 任意のHTTPクライアント (Web、モバイル、バックエンド) |
このフレームワークを使用して、意図的で正当なアーキテクチャ上の選択を行ってください。
useOptimisticのようなフックは、このユースケースにおいて比類のないものです。例のシナリオ: ブログ投稿の「いいね」ボタン。これはUI要素に直接結びついたシンプルなミューテーションです。楽観的更新が望ましく、CSRF保護が重要です。Server Actionは完璧なツールです。
POSTリクエストを送信するための安定した公開URLを必要とします。Route Handlersがこれを提供します。Cache-Controlヘッダーを201 Createdレスポンスに設定したり、複雑なCache-Controlディレクティブを実装したり、429 Too Many Requestsステータスで応答したりする必要がある場合、そのレベルの制御はServer Actionsではできません。Route Handlersはこれのために作られています。ReadableStreamを使用する機能は、パフォーマンスとユーザーエクスペリエンスにとって不可欠です。例のシナリオ: 自社のiOSアプリが利用する/api/products?category=shoesエンドポイントを提供する。アプリは標準のJSON REST APIを必要とし、Cache-Controlヘッダーでキャッシュを制御する必要がある。Route Handlerが唯一の正しい選択です。
はい、可能です。Server Actionは単なるエクスポートされた非同期関数です。Route Handlerファイルにインポートして直接呼び出すことができます。これはビジネスロジックを再利用するのに役立ちます。例えば、Webhook用のRoute Handlerが、UIが使用するのと同じcreatePost Server Actionを呼び出すかもしれません。
// app/api/webhook/route.ts
import { createPostFromWebhook } from '@/app/actions/post-actions';
export async function POST(req: Request) {
const payload = await req.json();
// Re-use the same core logic!
const result = await createPostFromWebhook(payload);
return Response.json(result);
}
Server ActionからRoute Handlerを呼び出すのも簡単です。それは、自社のアプリケーションのエンドポイントへの標準的なfetch呼び出しに過ぎません。
const data = await fetch('https://yourapp.com/api/some-data');
エラー処理モデルはかなり異なります。Server ActionsはUI統合のために設計されています。通常、アクション内でtry/catchブロックを使用します。エラーがスローされた場合、クライアントで捕捉され、UIの更新に使用できます。フォーム固有のエラーの場合、useFormStateフックがベストプラクティスです。これにより、アクションから構造化された状態オブジェクト(エラーを含む)を返し、それがコンポーネントの状態を更新します。
Route Handlersは、エラーに標準のHTTPセマンティクスを使用します。適切なHTTPステータスコード(例:不正な入力には400、未承認には401、サーバーエラーには500)と、エラーを説明するJSONボディを返すことでエラーを通知します。クライアントサイドのfetch呼び出しは、その後response.okをチェックし、2xx以外のステータスコードを適切に処理する必要があります。
Server Actionsを使用しない方が良い主なシナリオは3つあります。
201 Createdレスポンスに特定のLocationヘッダーを設定したり、複雑なCache-Controlディレクティブを実装したり、429 Too Many Requestsステータスで応答したりする必要がある場合、そのレベルの制御はできません。Route Handlersはこれのために作られています。POSTリクエスト経由で実行され、GETリクエストと同じ方法でHTTPキャッシュやCDNによってキャッシュすることはできません。データフェッチには、fetch呼び出しでasync/awaitを使用するServer Componentsを使用するか、クライアントサイドのフェッチにはRoute Handlersを使用してください。シニアエンジニアは、特定のHTTPメソッド(GET、POST、PUT、DELETE)と異なる操作に対するステータスコードを含む、RESTful原則に厳密に準拠する必要がある重要なサードパーティ統合のための新しいAPIエンドポイントを構築するタスクを任されています。統合パートナーは、エラー処理とリソース識別に関して標準的なHTTPセマンティクスを期待しています。このAPIを実装するために最も適切なNext.js 15の機能はどれですか、そしてその理由は?
Server ActionsとRoute Handlersは競合するものではなく、アプリケーションアーキテクチャの異なる層のための補完的なツールです。Next.js 15のフルスタックモデルは、Server Actionsの高レベルで統合されたRPCと、Route Handlersの低レベルで普遍的なプリミティブの両方を提供するため、強力です。
深いアーキテクチャ上の違いを理解し、意識的で情報に基づいた意思決定を行うことで、より堅牢で安全、かつ高性能なNext.jsアプリケーションを構築できます。
AI CLIログの整形、Cron式の作成、JWTの解析、chmod権限の計算をオフラインで実行。

Next.js15のセキュリティアーキテクチャを詳しく解説し、CSRF、SSRF、ステートインジェクションからServer Actionsを強化するための、実証済みの本番環境での例を含むアーキテクチャガイドです。
続きを読む
Next.js15のキャッシュコンポーネントとServer Actionsを網羅した、本番環境レベルのアーキテクチャとコード例を含む完全ガイドです。
続きを読む
Next.js14でServer Actions、useFormState、useFormStatus、Zodを使ったフォーム管理を詳しく解説し、クライアントサイドJavaScriptの負担なく、モダンでプログレッシブエンハンスメントなフォームを構築する方法を学びましょう。
続きを読む