4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

備忘録:Next.js App RouterでServer Actions方式とAPI方式をどう使い分けるか。判断基準と選定フローを整理する

4
Last updated at Posted at 2026-08-22

はじめに

Next.jsのApp Routerでデータ登録や更新を実装するとき、選択肢が主に2つあります。

  • Server Actions方式: ReactコンポーネントからServer Functionを呼び出す
  • API方式: route.ts にRoute Handlerを定義し、fetch() などでHTTPリクエストする

どちらもサーバー側でDB更新や認証処理を実行できます。
そのため、実装のたびに「どちらでも動くが、どちらが正しいのか」で止まっていました。
設計上の目的が違うので、「どちらでも動く」で決めると後から手戻りが出ます。

そこで、両方式の違いを実装・通信・認証・キャッシュ・エラー処理・再利用性の観点から整理し、最後に選定フローへ落としました。
毎回同じところで迷わないよう、判断基準を1か所にまとめた記録です。

環境は React 19 / Next.js App Router(React 19 に対応した Next.js 15 以降)/ TypeScript を前提にしています。
キャッシュや再検証の挙動はバージョンによって差があるので、最終的な確認はそのつど利用中のバージョンの公式ドキュメントで行います。
掲載するコードは判断基準を示すための最小例で、実務では監査ログやレート制限を別途足す必要があります。

1. 用語を整理する

Server FunctionとServer Action

Reactの現在の用語では、'use server'でクライアントから呼び出せるようにした非同期関数を、広くServer Functionと呼びます。

そのServer Functionを、フォームのactionやボタンのformActionなど、ReactのAction機構から呼び出す場合にServer Actionと呼びます。

Next.jsのドキュメントや既存記事では、両方をまとめて「Server Actions」と表現している場合もあります。
本記事では、一般的な呼び方に合わせて「Server Actions方式」と表記します。

API方式

App Routerでは、通常次のファイルにHTTPハンドラーを実装します。

app/api/users/route.ts

この仕組みの正式名称はRoute Handlerです。

Pages Routerの pages/api/... はAPI Routesと呼ばれるため、両者を混同しないよう注意します。

2. 通信経路の違い

Server Actions方式

2方式の通信経路の対比。Server Actions方式は、Client Component / form から Next.js が POST 通信を生成し、Server Action、Service / DB を経て、戻り値と更新後の RSC Payload を返す。URL・メソッド・本文は Next.js が組み立てる。API方式は、Client Component から fetch("/api/users") で Route Handler を呼び、Service / DB を経て JSON・ファイル・ストリームを返す。URL・メソッド・ヘッダー・本文を呼び出し側が明示する

テキスト版を開く
Server Actions 方式                 API 方式(Route Handler)
Client Component / form             Client Component
  ↓ Server Function への参照           ↓ fetch("/api/users")
Next.js が POST 通信を生成           Route Handler
  ↓                                   ↓
Server Action                       Service / DB
  ↓                                   ↓
Service / DB                        JSON / ファイル / ストリーム
  ↓
戻り値 + 更新後の RSC Payload

コード上では関数呼び出しに見えます。

<form action={createUser}>
  <input name="name" />
  <button type="submit">登録</button>
</form>

しかし、Client Componentから呼び出す場合は、実際にはブラウザとサーバー間でネットワーク通信が発生します。

API方式

Client Component
        ↓ fetch("/api/users")
Route Handler
        ↓
Service / DB
        ↓
JSON / ファイル / ストリームなどのHTTPレスポンス

呼び出し側がURL、HTTPメソッド、ヘッダー、本文、レスポンス処理を明示します。

const response = await fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name }),
})

3. 比較表

観点 Server Actions方式 API方式(Route Handler)
主な目的 Next.js UIからサーバー状態を更新する HTTPインターフェースを提供する
呼び出し方法 関数参照、form action fetch、curl、SDK、外部サービス
URL設計 通常は意識しない /api/v1/usersなどを設計する
HTTPメソッド 基本的に内部POST GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS
外部システム利用 不向き 適している
モバイルアプリ利用 不向き 適している
Webhook受信 不向き 適している
フォーム処理 非常に相性がよい 自前実装が増える
RSC再描画との統合 強い 呼び出し側で再取得処理が必要になりやすい
HTTPステータス制御 主目的ではない 細かく制御できる
レスポンス形式 シリアライズ可能な値 JSON、XML、ファイル、画像、ストリームなど
オリジン制御 既定で OriginHost を照合。必要なら serverActions.allowedOrigins を設定 CORSヘッダー・認証方式・CSRF対策を用途に合わせて設計
バージョニング 不向き /api/v1などで管理しやすい
API契約 React/Next.jsに依存 OpenAPIなどで明示しやすい
テスト 関数・ユースケース単位が中心 HTTPレベルの統合テストがしやすい
Progressive Enhancement Server Componentの<form action>なら組み込みで対応。Client Componentでは Hydration まで送信をキューする 通常のHTMLフォームでも可能。ただしGET/POST・フォーム形式・リダイレクトを自前で設計する
高頻度取得 不向き 適している
ファイル配信・SSE 不向き 適している

4. Server Actions方式の実装例

ユーザー登録を例にします。

Server Action

// app/users/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createUser } from '@/features/users/service'
import { requireAdmin } from '@/lib/auth'

export type CreateUserState = {
  success: boolean
  message: string
  errors?: {
    name?: string[]
  }
}

export async function createUserAction(
  _previousState: CreateUserState,
  formData: FormData,
): Promise<CreateUserState> {
  const session = await requireAdmin()

  const rawName = formData.get('name')

  if (typeof rawName !== 'string' || rawName.trim() === '') {
    return {
      success: false,
      message: '入力内容を確認してください',
      errors: {
        name: ['名前を入力してください'],
      },
    }
  }

  await createUser({
    operatorId: session.userId,
    name: rawName.trim(),
  })

  revalidatePath('/users')
  redirect('/users')
}

フォーム

// app/users/new/UserForm.tsx
'use client'

import { useActionState } from 'react'
import { createUserAction, type CreateUserState } from '../actions'

const initialState: CreateUserState = {
  success: false,
  message: '',
}

export function UserForm() {
  const [state, formAction, isPending] = useActionState(
    createUserAction,
    initialState,
  )

  return (
    <form action={formAction}>
      <label>
        名前
        <input name="name" />
      </label>

      {state.errors?.name?.map((message) => (
        <p key={message}>{message}</p>
      ))}

      <button type="submit" disabled={isPending}>
        {isPending ? '登録中...' : '登録'}
      </button>

      {state.message && <p aria-live="polite">{state.message}</p>}
    </form>
  )
}

この方式では、以下をReactとNext.jsの仕組みに統合できます。

フォーム送信
→ Server Action実行
→ バリデーション
→ DB更新
→ キャッシュ再検証
→ Server Component再実行
→ 更新後のUIを反映

5. API方式の実装例

同じユーザー登録をRoute Handlerで実装します。

Route Handler

// app/api/users/route.ts
import { NextResponse } from 'next/server'
import { createUser } from '@/features/users/service'
import { requireApiAdmin } from '@/lib/auth'

type CreateUserRequest = {
  name?: unknown
}

export async function POST(request: Request) {
  const session = await requireApiAdmin(request)

  let body: CreateUserRequest

  try {
    body = (await request.json()) as CreateUserRequest
  } catch {
    return NextResponse.json(
      {
        code: 'INVALID_JSON',
        message: 'JSON形式が不正です',
      },
      { status: 400 },
    )
  }

  if (typeof body.name !== 'string' || body.name.trim() === '') {
    return NextResponse.json(
      {
        code: 'VALIDATION_ERROR',
        message: '入力内容を確認してください',
        errors: {
          name: ['名前を入力してください'],
        },
      },
      { status: 422 },
    )
  }

  const user = await createUser({
    operatorId: session.userId,
    name: body.name.trim(),
  })

  return NextResponse.json(
    { user },
    { status: 201 },
  )
}

Client Componentから呼び出す

'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'

export function UserForm() {
  const router = useRouter()
  const [name, setName] = useState('')
  const [error, setError] = useState<string | null>(null)
  const [isPending, setIsPending] = useState(false)

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setError(null)
    setIsPending(true)

    try {
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ name }),
      })

      const result = await response.json()

      if (!response.ok) {
        setError(result.message ?? '登録に失敗しました')
        return
      }

      router.push('/users')
      router.refresh()
    } catch {
      setError('通信に失敗しました')
    } finally {
      setIsPending(false)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={(event) => setName(event.target.value)}
      />

      <button type="submit" disabled={isPending}>
        {isPending ? '登録中...' : '登録'}
      </button>

      {error && <p>{error}</p>}
    </form>
  )
}

API方式では自由度が高い一方、次の処理を呼び出し側で実装します。

  • fetch()の構築
  • JSONへの変換
  • HTTPステータスの判定
  • 通信エラー処理
  • Pending状態
  • 成功後の画面遷移
  • RSCの再取得やクライアントキャッシュ更新

6. 最も重要な判断基準

次の質問から判断すると迷いにくくなります。

この処理をNext.jsの画面以外から呼び出す必要があるか?

呼び出さない

Server Actionsが第一候補です。

例を挙げます。

  • 管理画面の商品登録
  • プロフィール編集
  • 予約の登録・変更・キャンセル
  • 承認・却下
  • お問い合わせフォーム
  • ユーザー設定変更
  • コメント投稿

呼び出す可能性がある

Route Handlerが第一候補です。

例を挙げます。

  • モバイルアプリ
  • 別ドメインのフロントエンド
  • 外部SaaS
  • IoTデバイス
  • Webhook
  • OAuthコールバック
  • 公開API
  • パートナー向けAPI
  • バッチやCLIからのHTTP呼び出し

7. データ取得はどうするか

Server Componentからの取得

同じNext.jsアプリ内であれば、Server Componentから自分自身のRoute Handlerを呼ばず、ServiceやRepositoryを直接呼ぶのが基本です。

// app/users/page.tsx
import { listUsers } from '@/features/users/service'

export default async function UsersPage() {
  const users = await listUsers()

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

避けたい構成は次のとおりです。

Server Component
  → 自分自身のRoute HandlerへHTTP通信
  → Service
  → DB

推奨構成は次のとおりです。

Server Component
  → Service
  → DB

自分自身のRoute Handlerを経由すると、HTTPのシリアライズ、認証情報の受け渡し、エラー変換など、不要な処理が増えます。

Client Componentからの取得

ブラウザ上で動的にデータを取得する必要がある場合は、Route Handlerが適しています。

例を挙げます。

  • 検索サジェスト
  • 無限スクロール
  • ポーリング
  • TanStack QueryやSWRによる取得
  • ブラウザから任意タイミングで再取得

Server Functionsはサーバー状態の更新を主目的としており、データ取得APIの代替として多用するものではありません。

8. キャッシュと再描画の違い

Server Actions方式

Server Actionでは、DB更新後にNext.jsの再検証APIを呼び出せます。

revalidatePath('/users')

または、タグ単位のキャッシュ戦略を採用している場合は、タグに対応した再検証を行います。

Server Action のレスポンスでは、Action の結果に加えて、再検証で更新された UI の RSC Payload を同じ往復で返せます。
常に返るわけではなく、再検証など再描画が必要な操作と組み合わせたときの能力です。

API方式

Route HandlerからもrevalidatePath()などを呼べますが、ブラウザ側のUIが自動的に最新状態へ切り替わるとは限りません。

ここで自分が取り違えていたのは、この2つを「どれか1つやればよい」と並べていたことでした。
実際には順番に両方やります。

  1. Route Handler 側で revalidatePath()revalidateTag() を呼び、サーバーキャッシュを無効化する
  2. クライアント側で遷移・router.refresh()・SWR の mutate()・TanStack Query の invalidateQueries() などを行い、表示を更新する

router.refresh() は現在のルートを再取得する操作で、サーバー側のキャッシュは無効化しません。
1をやらずに2だけ行うと、キャッシュされた古いデータがそのまま返ってきます。

なお Route Handler から呼んだ revalidatePath() は、表示中のUIを即座に更新するわけではなく、対象パスを次回アクセス時の再検証対象にします。
Server Function から呼んだ場合は、そのパスを表示中ならUIも直ちに更新されます。

つまり API方式では、サーバーキャッシュの無効化とクライアント表示の更新を別々に設計する必要があります。

9. エラー処理の設計

Server Actions方式

入力エラーや業務上想定されるエラーは、戻り値として返すと扱いやすくなります。

return {
  success: false,
  message: '入力内容を確認してください',
  errors: {
    name: ['名前を入力してください'],
  },
}

一方、想定外の障害はthrowし、error.tsxやError Boundaryへ渡します。

throw new Error('Database connection failed')

基本方針は次のとおりです。

想定内のエラー
  → ActionStateとして返す

想定外の障害
  → throwしてError Boundaryへ

API方式

HTTP APIとして意味のあるステータスコードを返します。

400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
409 Conflict
422 Unprocessable Content
500 Internal Server Error

API利用者が複数存在する場合は、レスポンス形式も契約として固定します。

{
  "code": "RESERVATION_CONFLICT",
  "message": "指定期間は予約済みです",
  "details": {
    "vehicleId": "vehicle-001"
  }
}

HTTPレベルの表現力が必要なら、Route Handlerが適しています。

10. 認証・認可はどちらでも必須

Server ActionはURLが見えにくいため、内部関数のように感じます。
しかし、Client Componentから呼び出す場合はネットワーク経由のエンドポイントです。

次の考え方は危険です。

画面上に削除ボタンがないから呼ばれない
TypeScriptで型を指定したから不正値は来ない
Server Actionだからログイン済みのはず

Server Actionの引数はクライアントから操作できます。
必ずAction内部で確認します。

'use server'

export async function deleteUserAction(userId: string) {
  const session = await requireSession()

  if (session.role !== 'ADMIN') {
    throw new Error('Forbidden')
  }

  const user = await db.user.findUnique({
    where: { id: userId },
  })

  if (!user) {
    return {
      success: false,
      message: 'ユーザーが存在しません',
    }
  }

  await db.user.delete({
    where: { id: userId },
  })

  revalidatePath('/users')

  return {
    success: true,
    message: '削除しました',
  }
}

次の対策は、どちらの方式でも必要です。

  • 認証
  • 認可
  • 入力検証
  • 所有権・テナント境界の確認
  • レート制限
  • 監査ログ
  • 機密情報の出力制御

一方で、リクエスト元の検証は方式によって前提が違います。
ここを「どちらも同じ」と書いていたのは不正確でした。

  • Server Actions には、POST のみを許可し OriginHost を照合する組み込みの CSRF 対策があります。別オリジンを許すなら serverActions.allowedOrigins を設定します。
  • Cookie 認証の Route Handler では、CORS を設定しても CSRF 対策にはなりません。CORS は主にブラウザによるレスポンス読み取りを制御する仕組みで、単純リクエストの送信自体は止められないからです。SameSite に加えて CSRF トークンや Origin 検証を自分で設計します。

11. Server Actionsが向かないケース

Server Actionsは便利ですが、すべてのサーバー通信を置き換えるものではありません。

次の用途ではRoute Handlerを選択します。

Webhook

外部サービスがURLへPOSTするため、Route Handlerが必要です。

// app/api/webhooks/payment/route.ts
export async function POST(request: Request) {
  const signature = request.headers.get('x-signature')
  const rawBody = await request.text()

  await verifySignature(signature, rawBody)
  await processPaymentEvent(rawBody)

  return new Response(null, { status: 204 })
}

ファイルやストリーム

  • CSVダウンロード
  • PDF出力
  • 画像変換
  • SSE
  • AIのストリーミングレスポンス
  • 大容量ファイルアップロード

これらはHTTPレスポンスを直接制御できるRoute Handlerが適しています。

高頻度のデータ取得

  • オートコンプリート
  • 数秒単位のポーリング
  • チャートデータ取得
  • IoTテレメトリー
  • 無限スクロール

Server Functionsは更新処理向けであり、一般的な取得APIとして使うべきではありません。

12. 共通業務ロジックをService層へ分離する

実務では、Server ActionとRoute Handlerのどちらを選ぶか以上に、業務ロジックをどこへ置くかが重要です。

推奨構成は次のとおりです。

レイヤー構成図。UI層に Server Action(Next.js画面専用の更新)と Route Handler(外部クライアント向けHTTP)が並び、どちらも Application Service / Use Case へ集まる。その下に Domain Logic、Repository / Database が続く。Server Action は UI 向けアダプター、Route Handler は HTTP 向けアダプターとして扱う

テキスト版を開く
UI
├── Server Action    (Next.js 画面専用の更新)
└── Route Handler    (外部クライアント向け HTTP)
        ↓
Application Service / Use Case
        ↓
Domain Logic
        ↓
Repository / Database

Service

// features/users/service.ts
type CreateUserInput = {
  operatorId: string
  name: string
}

export async function createUser(input: CreateUserInput) {
  await assertOperatorCanCreateUser(input.operatorId)

  const duplicated = await db.user.findFirst({
    where: {
      name: input.name,
    },
  })

  if (duplicated) {
    throw new UserNameConflictError(input.name)
  }

  return db.user.create({
    data: {
      name: input.name,
      createdBy: input.operatorId,
    },
  })
}

Server ActionはUI向けアダプターにします。

FormDataを受け取る
→ UI用の入力検証
→ 認証情報を取得
→ Serviceを呼ぶ
→ UI用の戻り値を返す
→ RSCを再検証する

Route HandlerはHTTP向けアダプターにします。

Requestを受け取る
→ JSONを解析
→ API認証
→ Serviceを呼ぶ
→ HTTPステータスとJSONへ変換

この構成なら、後からAPIを追加しても業務ロジックを書き直す必要がありません。

13. 典型的なアンチパターン

1. Server Actionに業務ロジックを全部書く

'use server'

export async function reserveAction(formData: FormData) {
  // 認証
  // 日付計算
  // 空き確認
  // 料金計算
  // クーポン判定
  // 決済
  // メール送信
  // DB更新
  // 監査ログ
}

Actionが巨大化すると、APIやバッチから再利用できず、単体テストも難しくなります。

2. Server Componentから自分のAPIを呼ぶ

公開URLへの余分なHTTP往復が増えます。
本番で同一プロセスとは限らず、事前レンダリング時にはサーバーが待ち受けていないためビルドが失敗することもあります。
Server Component からは Service を直接呼びます。

3. 外部公開予定があるのにServer Actionだけで作る

モバイルアプリや外部連携が確定しているなら、最初からHTTP API契約を設計した方が安全です。

4. Server Actionだから認可を省略する

Server Actionはネットワーク境界です。
画面の表示制御とサーバー側認可は別問題です。

5. API方式に統一しすぎる

Next.js画面専用の単純なフォームまで、すべてRoute Handlerとfetch()で実装すると、状態管理や再描画処理が増えます。

14. 選定フロー

選定フローチャート。まずサーバー状態を更新する処理かを問う。いいえなら、Server Componentから取得できるかを問い、はいなら Service / DB を直接呼ぶ、いいえなら Route Handler。はいなら、Next.js画面以外から呼ばれる可能性があるかを問い、はいなら Route Handler、いいえなら Server Action。ただしWebhook・ファイル配信・SSE・CORS制御が要るなら、更新処理でも Route Handler を選ぶ

テキスト版を開く
処理はサーバー状態の更新か?
├── いいえ
│   ├── Server Component で取得できる
│   │   → Service / DB を直接呼ぶ
│   └── ブラウザから動的に取得する
│       → Route Handler
│
└── はい
    ├── Next.js 画面以外から呼ばれるか?
    │   ├── はい → Route Handler
    │   └── いいえ → Server Action
    │
    └── Webhook・ファイル・ストリーム・CORS 制御が必要か?
        ├── はい → Route Handler
        └── いいえ → Server Action

15. 実務上の推奨構成

一般的な業務システムでは、どちらか一方へ統一するのではなく、役割ごとに併用します。

Server Component
  表示用データをServiceから直接取得

Client Component
  入力、イベント、ブラウザ状態を管理

Server Action
  Next.js画面専用の登録・更新・削除

Route Handler
  外部API、Webhook、取得API、ファイル、ストリーム

Service / Use Case
  共通する業務ロジック

Repository
  DBアクセスを抽象化

最終的な判断を一文でまとめると、次のようになります。

UIの更新処理としてReactに統合したいならServer Actions、HTTPインターフェースとして提供したいならRoute Handlerを使う。

参考資料

4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?