•24 min read

Next.js15におけるServerActionsとRouteHandlers:詳細なアーキテクチャ比較

Next.js15におけるServerActionsとRouteHandlers:詳細なアーキテクチャ比較

Next.jsを使った最新のフルスタック開発では、クライアントとサーバーの境界は意図的に曖昧にされています。この融合は開発者の生産性を飛躍的に向上させますが、同時に重要なアーキテクチャ上の問いを投げかけます。サーバーサイドロジックをどこで実行すべきか、という問いです。具体的には、ミューテーション(データの変更)やデータフェッチにおいて、従来のRoute HandlerとServer Actionのどちらを選ぶべきでしょうか?これは好みの問題ではなく、パフォーマンス、セキュリティ、ユーザーエクスペリエンスに深く影響する根本的なアーキテクチャ上の決定です。誤った選択は、プログレッシブエンハンスメントの欠如、キャッシングの問題、セキュリティの脆弱性を抱える脆いアプリケーションにつながる可能性があります。この記事では、シニアエンジニアが常に正しい判断を下すために必要な、深く権威ある分析を提供します。

Audio Briefing
0:00 / 0:00

核となる二分法:RPC vs. REST

最も高いレベルでは、この区別は通信パラダイムの違いにあります。

  • Server Actionsは**Remote Procedure Call (RPC)**モデルを実装しています。クライアントサイドのコードは、サーバー上で実行される関数を呼び出します。ネットワーク境界、HTTPメソッド、データシリアライゼーションは、Next.jsフレームワークによってほとんど抽象化されています。インタラクションの主要な単位は関数シグネチャです。

  • Route Handlersは**Representational State Transfer (REST)**またはRESTライクなモデルを実装しています。クライアントが標準的なHTTPセマンティクスを使用してインタラクトする、個別のHTTPエンドポイント(GET、POST、PUT、DELETEなど)を作成します。ネットワークは明示的です。インタラクションの主要な単位はURLとHTTPメソッドです。

この根本的な違いが、キャッシングの挙動からセキュリティ体制まで、その後のすべてを決定します。それぞれを詳しく見ていきましょう。

Advertisement

詳細解説:Next.js Server Actions

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>
  );
}

内部の仕組み:

  1. JavaScriptなしの場合: クライアントのJavaScriptが無効になっているか、まだロードされていない場合、これは標準的なHTMLフォームの送信として機能します。ブラウザはフォームデータをシリアライズし、現在のURLにPOSTリクエストを送信します。Next.jsサーバーはこれを受け取り、ターゲットのServer Actionを特定して実行し、その後ページを再レンダリングします。これは古典的で堅牢なウェブの挙動です。
  2. JavaScriptありの場合: Reactがハイドレーションされると、Next.jsはフォームの送信をハイジャックします。ページ全体の再読み込みの代わりに、特別なエンドポイントにfetch POSTリクエストを行います。このリクエストのボディはJSONではなく、FormDataです。Server Actionの一意のID(ビルド時に生成されるハッシュ)は、URLまたは特別なNext-Action HTTPヘッダーに含まれてリクエストに送られます。サーバーはこのリクエストを正しい関数にルーティングし、実行し、戻り値がクライアントにストリーミングされます。Reactはこのデータを使用して、ページ全体のナビゲーションなしでUIを更新します。

ページ全体の再読み込みからクライアントサイドのナビゲーションへのこのシームレスなアップグレードは、プログレッシブエンハンスメントの本質であり、DX/UXにおける大きな勝利です。

RPCプロトコルとクロージャのシリアライゼーション

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に明示的に伝える責任を負います。

セキュリティ:組み込みのCSRF保護

これは、しばしば見過ごされがちな重要な利点です。Server Actionsには、組み込みのクロスサイトリクエストフォージェリ(CSRF)保護があります。

Server Actionが定義されると、Next.jsは一意の、アクションごとのIDを埋め込みます。クライアントから呼び出されると、このIDはNext-Action HTTPヘッダーで送信されます。サーバーでは、受信リクエストのアクションIDがサーバーの既知のアクションIDと照合されます。さらに、フレームワークは、リクエストが自サイトから発信されたことを保証するために、トークンベースの戦略(ダブルサブミットクッキーパターンと比較されることが多いが、内部で管理される)を採用しています。これは自動的に行われ、開発者による設定は不要で、ミューテーションに対してデフォルトで安全な状態を提供します。

詳細解説:Next.js Route Handlers

Route Handlersは、Pages RouterのAPI Routesの精神的後継です。これらはapp/api/.../route.tsファイルに存在し、標準のWeb RequestおよびResponse APIを活用することで、HTTPリクエストとレスポンスを完全に低レベルで制御できます。

RESTパラダイム:明示的で普遍的

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ヘッダーの設定。
  • ボディ: JSON、テキスト、FormDataの読み書き、さらにはデータのストリーミング。

キャッシング:きめ細やかなHTTP制御

Route Handlersでのキャッシングは明示的であり、HTTP標準に従います。以下を通じて制御します。

  1. ルートセグメント設定:
    // Statically evaluate at build time
    export const dynamic = 'force-static';
    // Always dynamically evaluate
    export const dynamic = 'force-dynamic';
    
  2. 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',
      },
    });
    
  3. Next.jsデータキャッシュ: 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はセキュリティの負担を開発者に全面的に負わせます。

  • CSRF: 組み込みのCSRF保護はありません。POST、PUT、またはDELETEハンドラーが機密性の高い操作を実行し、セッションベースである場合、CSRF対策(例:ダブルサブミットクッキー、シンクロナイザートークンパターン)を実装する責任があります。
  • CORS: デフォルトでは、Route Handlersは同一オリジンです。クロスオリジンリクエスト(例:サードパーティサイトが利用する公開APIの場合)を許可する必要がある場合は、レスポンスでCORSヘッダーを手動で設定する必要があります。
  • 認証/認可: リクエストを認証および認可するために、ヘッダー(Authorization: Bearer ...)、クッキー、またはその他のトークンを手動で検査する必要があります。

アーキテクチャ実行フローの比較

視覚的な比較により、異なる実行パスが明確になります。

graph TD
    subgraph Client
        direction LR
        A[React Component UI]
    end

    subgraph "Server Action Flow (RPC)"
        direction TB
        B(Form Submit / onClick) --> C{JS Loaded?};
        C -- Yes --> D[fetch POST w/ FormData & Next-Action header];
        C -- No --> E[Full Page POST Request];
        D --> F[Next.js Server];
        E --> F;
        F --> G[Find & Execute Server Action fn];
        G --> H[Revalidate Cache];
        H --> I[Return Data/Redirect];
        I --> A;
    end

    subgraph "Route Handler Flow (REST)"
        direction TB
        J(Client-side fetch) --> K[GET/POST /api/path];
        K --> L[Next.js Server];
        L --> M[Execute Route Handler fn];
        M --> N[Return Response object (JSON, text, stream)];
        N --> J;
        J --> A;
    end

    style F fill:#582ABC,stroke:#FFF,stroke-width:2px,color:#FFF
    style M fill:#582ABC,stroke:#FFF,stroke-width:2px,color:#FFF
    style G fill:#713AF0,stroke:#FFF,stroke-width:1px,color:#FFF
    style H fill:#713AF0,stroke:#FFF,stroke-width:1px,color:#FFF
Advertisement

詳細比較マトリックス

機能Server ActionsRoute 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、モバイル、バックエンド)

意思決定フレームワーク:どちらをいつ使うべきか?

このフレームワークを使用して、意図的で正当なアーキテクチャ上の選択を行ってください。

Server Actionsを選択する場合:

  1. フォーム送信とUIミューテーションの処理: これがServer Actionsの得意分野です。フォームとの密接な統合、プログレッシブエンハンスメント、およびuseOptimisticのようなフックは、このユースケースにおいて比類のないものです。
  2. 呼び出し元がNext.js Reactコンポーネントである場合: Server Actionsは、アプリケーションのUIから呼び出されるように設計されています。開発者体験(DX)はシームレスです。
  3. デフォルトで安全なミューテーションを望む場合: 組み込みのCSRF保護は、セキュリティと生産性において大きな恩恵をもたらします。それについて考える必要はありません。
  4. ロジックがUIコンポーネントと密接に結合している場合: アクションをそれを使用するコンポーネント内またはその隣に定義することで、優れたコードのコロケーションと保守性が生まれます。

例のシナリオ: ブログ投稿の「いいね」ボタン。これはUI要素に直接結びついたシンプルなミューテーションです。楽観的更新が望ましく、CSRF保護が重要です。Server Actionは完璧なツールです。

Route Handlersを選択する場合:

  1. 公開APIまたはサードパーティAPIを構築する場合: 他のアプリケーション(Next.jsフロントエンドではないもの)がデータを消費する必要がある場合、標準のHTTPエンドポイントが必要です。これがRoute Handlersの主要な役割です。
  2. Webhookを受信する場合: Stripe、GitHub、TwilioなどのサービスからのWebhookは、POSTリクエストを送信するための安定した公開URLを必要とします。Route Handlersがこれを提供します。
  3. HTTPをきめ細かく制御する必要がある場合: 特定のCache-Controlヘッダーを201 Createdレスポンスに設定したり、複雑なCache-Controlディレクティブを実装したり、429 Too Many Requestsステータスで応答したりする必要がある場合、そのレベルの制御はServer Actionsではできません。Route Handlersはこれのために作られています。
  4. Next.js以外のクライアントのデータソースとして機能する場合: モバイルアプリのフロントエンド、SvelteKitアプリ、あるいはカスタムフェッチャーを持つSWR/React Queryのようなライブラリを使用している場合でも、Route Handlersは必要なRESTfulデータソースを提供します。
  5. 大規模なレスポンスをストリーミングする場合: AIを活用したチャットボットや大規模なデータセットの処理では、Route Handlerで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つあります。

  1. 公開APIの場合: Server Actionsは、安定した公開エンドポイントとして設計されていません。その呼び出し規約はNext.jsの内部実装の詳細であり、変更される可能性があります。また、Next.js以外のクライアントからは発見も使用もできません。
  2. きめ細やかなHTTP制御が必要な場合: 201 Createdレスポンスに特定のLocationヘッダーを設定したり、複雑なCache-Controlディレクティブを実装したり、429 Too Many Requestsステータスで応答したりする必要がある場合、そのレベルの制御はできません。Route Handlersはこれのために作られています。
  3. GETリクエストの場合: データをフェッチするためにServer Actionを呼び出すことは可能ですが、これはアンチパターンです。Server ActionsはPOSTリクエスト経由で実行され、GETリクエストと同じ方法でHTTPキャッシュやCDNによってキャッシュすることはできません。データフェッチには、fetch呼び出しでasync/awaitを使用するServer Componentsを使用するか、クライアントサイドのフェッチにはRoute Handlersを使用してください。

結論:2つのツール、1つの目標

Server ActionsとRoute Handlersは競合するものではなく、アプリケーションアーキテクチャの異なる層のための補完的なツールです。Next.js 15のフルスタックモデルは、Server Actionsの高レベルで統合されたRPCと、Route Handlersの低レベルで普遍的なプリミティブの両方を提供するため、強力です。

  • Next.jsアプリケーションのUI内から開始されるすべてのミューテーションと操作には、Server Actionsをデフォルトで使用してください。プログレッシブエンハンスメント、統合された状態管理、自動CSRF保護の利点は無視できないほど大きいです。
  • Next.jsエコシステムから抜け出す必要がある場合(他のクライアントにサービスを提供する場合、Webhookを受信する場合、HTTPプロトコルを正確に制御する場合)は、Route Handlersを利用してください。これらはより広範なウェブへのゲートウェイとなります。

深いアーキテクチャ上の違いを理解し、意識的で情報に基づいた意思決定を行うことで、より堅牢で安全、かつ高性能なNext.jsアプリケーションを構築できます。

さらに読む

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement