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の設定が必要なケース)
- ストリーミング: 大量データをチャンクで返したり、バイナリを直接操作するような低レイヤーな制御
- 認証の分離: 認証プロバイダーとの高度なハンドシェイク
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%説明できる自信がないなら、今日から設計をゼロベースで見直してみろ。それが、生き残るエンジニアの流儀だ。