🆚

Next.jsのルートハンドラーとサーバーアクションの使い分けを徹底比較

に公開

はじめに

Next.jsにはRoute Handlers(ルートハンドラー)Server Actions(サーバーアクション) という仕組みがあります。
この記事では、それぞれの特徴を比較して解説します。以下ではカタカナ表記を使います。

↓ルートハンドラーとサーバーアクションに関しては、過去に記事を書いています。
ルートハンドラーに関する記事
サーバーアクションに関する記事

この記事は他の記事や動画を調べてまとめ、最後に少し僕の意見を交えただけの記事です。
なので使い時に関しては絶対ではありません。
参考程度にお読みください。ルートハンドラーとサーバーアクションを選定する際の一助になれば幸いです。

この記事の対象者

・ルートハンドラーとサーバーアクションの使い分けを知りたい人
・ルートハンドラーとサーバーアクションの違いをざっくり知りたい人

ルートハンドラーとサーバーアクションの違い

ルートハンドラーとサーバーアクションの違いを、構文例で確認します。

ルートハンドラーのコード例

route.ts
// src\app\api\post\route.ts
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  const { message } = await req.json();
  console.log("受信メッセージ:", message);

  return NextResponse.json({ success: true, message });
}

page.tsx
"use client";
// src\app\routeHandler\page.tsx
import React, { useState } from "react";

const Page = () => {
  const [message, setMessage] = useState<string>("");

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();

    await fetch("/api/post/", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message }),
    });
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <label htmlFor="message">メッセージ</label>
        <input
          type="text"
          name="message"
          onChange={(e) => setMessage(e.target.value)}
        />
        <button type="submit">送信</button>
      </form>
    </div>
  );
};

export default Page;

サーバーアクションのコード例

action.ts
"use server";
// src\app\actions\action.ts

export async function sendMessage(formData: FormData) {
  const message = formData.get("message");
  console.log("受信メッセージ:", message);
}
// src\app\sendMessage\page.tsx
import React from "react";
import { sendMessage } from "../actions/action";

const page = () => {
  return (
    <div>
      <form action={sendMessage}>
        <label htmlFor="message">メッセージ</label>
        <input type="text" name="message" />
        <button type="submit">送信</button>
      </form>
    </div>
  );
};

export default page;

ルートハンドラーでは、fetch()を使ってHTTPベースで通信を行います。
そのため、外部サービスから直接エンドポイントを叩くことも可能です。

一方、サーバーアクションは関数を呼び出すだけ処理が完結します。
余分な HTTP 通信やエンドポイント定義が不要なため、コードをすっきりと書けるのが特徴です。

使い分け

調べてみた自分の結論は以下の表のようになりました。
他の方の動画や記事、AIを使って調べてまとめたので、こちらが必ず正解というわけではありません。参考程度に見ていただければと思います。

利用ケース ルートハンドラー サーバーアクション
ストリーミング ×
Webhook ×
C/U/D 操作
フォームの送信
カスタムヘッダー
CORS ×

ストリーミング

ストリーミングに関してですが、調べてみると海外のYoutuberさんは、よくAI Streamingとして紹介していました。
AI Streamingをまとめてみると以下のような感じです。
・LLM(例: OpenAI, Anthropic など)のAPIは トークン単位で部分的なレスポンスを返せる
・これをストリーミングで受け取ることで、ユーザーは 回答が完成する前に読み始められる
・ChatGPTのように文字が一文字ずつ出てくる挙動はこれ
ルートハンドラーでは下記のようにオプションを設定してストリーミングに対応することができますが、サーバーアクションではまだ対応していないそうです。
したがって、ストリーミングの機能を使う際は、ルートハンドラーに軍配が上がります。

return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });

Webhook

Webhookとは、アプリケーションやサービスで特定のイベントが発生した際に、その情報をリアルタイムで別のアプリケーションに通知する仕組みです。
例えば、GitHubのpushイベントを感知して、slackに通知を送る機能などがあたります。
Webhookの仕組みとしては、発信元(GitHubなど)がイベントを発生させると、自動で指定されたURLにHTTPリクエストを送ります。
つまり、受け取り側には 公開されたエンドポイント(API URL) が必要になりますので、外部からアクセスすることのできるルートハンドラーに軍配が上がります。

C/U/D操作

CUD とはCRUDのうちCreate/Update/Deleteを指します。
サーバーアクションは、Next.jsが「関数呼び出し」を裏でPOSTリクエストにシリアライズして送る仕組みのため、基本的にPOST 相当で動作します。
そのため、新規作成・更新・削除・フォーム送信などのデータ変更系処理に適しています。
サーバーアクションはミューテーションを得意としており、ルートハンドラーより簡潔に書けるため、CUDに関しては、サーバーアクションを推奨しています。
一方でサーバーアクションは、GET(データ取得)には向いていません。

なぜサーバーアクションでGETが非推奨なのか

・通信モデルに合わない
サーバーアクションは必ずPOSTで送られるため、データ取得をわざわざPOSTで行うのはHTTPセマンティクスに反します。

・キャッシュが効かない
Next.jsのfetch()なら自動的にキャッシュ・再検証(revalidate)が効きますが、サーバーアクションは毎回POSTになるためキャッシュが使えず、パフォーマンス低下につながります。

フォームの送信

フォームの送信は大概がミューテーションの操作で、先ほど説明したCUDに該当するケースが多いと思います。
また、先ほど記載したルートハンドラーとサーバーアクションの構文の比較をみるとわかると思いますが、サーバーアクションの方がaction属性に関数をセットするだけで、かなりスッキリと楽に書けると思います。

カスタムヘッダーやCORS

ヘッダーを細かく指定したいなら、ルートハンドラーが適していると思います。なぜなら、ルートハンドラーはHTTPレイヤーにアクセスすることができ、以下のように細かくヘッダーを設定することができるからです。

import { NextResponse } from "next/server";

export async function GET() {
  return NextResponse.json(
    { message: "Hello" },
    { headers: { "X-Custom-Header": "my-value" } }
  );
}

また、CORSに関しても、以下のようにヘッダー設定することで許可することができます。

import { NextResponse } from "next/server";

export async function GET() {
  return NextResponse.json(
    { message: "ok" },
    {
      headers: {
        "Access-Control-Allow-Origin": "*", // 必要に応じてドメインを制限
        "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
      },
    }
  );
}

サーバーアクションはfetchをせずそのままDBを操作したりするので、基本的に上記のようなヘッダーの設定などはできないと思われます。

結論の整理

結論を一文でまとめると
「UIからのデータ操作はサーバーアクション、HTTPエンドポイントが必要ならルートハンドラー」

最後に

本記事に目を通していただきありがとうございました。
今回はルートハンドラーとサーバーアクションの使い分けをメインに扱いました。
自分で実装した経験から書いた記事ではないので、間違っている箇所あればご指摘いただきたいです。

参考
https://www.youtube.com/watch?v=NWx8oVLEdwE&t=17s
https://www.youtube.com/watch?v=4pmPaAFB6ik&t=81s
https://zenn.dev/electnoob/articles/f02e12e4066fd4
https://form.run/media/contents/business-efficiency/about-webhook/

Discussion