Next.js Server Actions:その「便利さ」という名の劇薬をどう使いこなすか

Next.js Server Actions:その「便利さ」という名の劇薬をどう使いこなすか

「Server Actionsを使えばAPI Routeなんて不要だ」――そんな甘い言葉を信じて、アプリのすべてを`'use server'`で埋め尽くしたことはないか?

俺も最初はそうだった。コードは短くなるし、型は繋がるし、まるで魔法かと思った。だが、現場で泥臭く運用してみればわかる。これは諸刃の剣だ。適当な設計で実装すれば、アプリのパフォーマンスは死に、デバッグは迷宮入りし、半年後には「誰が書いたんだこのスパゲッティコードは」と自分を呪うことになる。

この記事では、Server ActionsとAPI Routeの境界線、そして俺が現場で培った「負債を生まないための防御的実装」を包み隠さず共有する。


1. 「API Routeは死んだ」という幻想

結論から言おう。API Routeは死んでいないし、むしろ「境界線」を意識できないエンジニアこそが死ぬ。

Server Actionsは確かに、クライアントとバックエンドを直結する最強のブリッジだ。しかし、以下のユースケースではAPI Routeを使うのが唯一の正解だ。

  • 外部連携: 外部サービスからのWebhook受取(CORSの設定が必要なケース)
  • ストリーミング: 大量データをチャンクで返したり、バイナリを直接操作するような低レイヤーな制御
  • 認証の分離: 認証プロバイダーとの高度なハンドシェイク
Server Actionsを「単なるAPIの代替」と捉えるな。あれは「UIの状態変化をサーバーで完結させるための手段」だ。外部との境界線は、依然としてAPI Routeに任せるのが、堅牢なアーキテクチャというものだ。

2. 「コンポーネント分離の罠」を突破する

`useFormStatus`が「直近の親」しか見ない仕様、あれには本当に手を焼いた。コンポーネントを細かく切り出すと、途端にボタンが反応しなくなる。

解決策はシンプルだ。「ロジックとUIを分離し、Propsで状態を流し込む」こと。そして、Server Actions自体をコンポーネントと同階層に書かず、`actions/`ディレクトリに隔離してテスト可能な状態にする。

推奨される設計パターン

// actions/profile.ts
import { z } from 'zod'

const schema = z.object({ name: z.string().min(1) })

// prevStateをanyにしない。型安全こそがServer Actionsの存在意義
export type ActionState = { message: string; success: boolean } | null

export async function updateProfile(
  prevState: ActionState, 
  formData: FormData
): Promise<ActionState> {
  const result = schema.safeParse(Object.fromEntries(formData))
  if (!result.success) return { message: '入力値エラー', success: false }
  
  // DB処理...
  return { message: '更新成功!', success: true }
}

UI側では、このアクションを呼び出すためのラッパーを1枚挟むだけで、責務が明確になる。


3. 「認証漏れ」を物理的に防ぐ:高階関数(HOA)の活用

`'use server'`を書き忘れて、裏口からデータが漏れる……そんな悪夢は、高階関数による強制ガードで撲滅できる。

// lib/auth-action.ts
export function authenticatedAction<T>(
  action: (data: T) => Promise<any>
) {
  return async (data: T) => {
    const session = await getSession()
    if (!session) throw new Error('Unauthorized')
    return action(data)
  }
}

// 使うときはこうする
export const updateProfile = authenticatedAction(async (data: Data) => {
  // ここに来る時点で認証は確定している
  await db.user.update(...)
})

こうすれば、個別の関数で`getSession`を書き忘れるリスクはゼロになる。防御的プログラミングとは、性善説を捨て、仕組みで縛ることだ。


4. キャッシュ戦略:タグという名の外科手術

`revalidatePath('/', 'layout')`で全キャッシュを破棄するような怠慢は今すぐやめろ。サーバー負荷が跳ね上がる原因だ。

使うべきは`revalidateTag`だ。

// 命名規則: [リソース名]-[ID]
revalidateTag(`user-profile-${userId}`)

キャッシュ不整合に陥った時のデバッグ術

もしデータが更新されないなら、`next/cache`の挙動を疑え。 `DEBUG=next:cache`を環境変数に入れて起動すれば、どのタグがいつ破棄されたかログが流れる。このログを読み解く能力こそ、Next.jsエンジニアの腕の見せ所だ。

最後に:Server Actionsは「劇薬」である

Server Actionsは、フロントエンドエンジニアにバックエンドの力を与える。しかし、その力は「コンポーネントとビジネスロジックの癒着」という副作用を伴う。

1. ロジックは必ず別ファイルへ切り出す。
2. 認証は高階関数で強制する。
3. キャッシュはタグ単位で外科手術のように制御する。

これらを守れないなら、Server Actionsは使うな。API Routeで大人しく通信していた方が、よほどマシなアプリができる。

技術は「便利だから使う」のではない。「制御できるから使う」のだ。自分のコードの挙動を100%説明できる自信がないなら、今日から設計をゼロベースで見直してみろ。それが、生き残るエンジニアの流儀だ。