Next.js Server Actionsで自爆した話。API Routeを捨てる前に知っておくべき「境界線」
「Server ActionsさえあればAPI Routeなんてオワコン。開発体験(DX)が爆速になる!」
そう信じて疑わなかった。個人開発のダッシュボードを全面移行し、意気揚々とデプロイした翌朝。ログを確認して背筋が凍りついた。`INSERT`文の嵐。ユーザーが投稿ボタンを連打した結果、DBには全く同じ内容のレコードが10件も並んでいた。
トランザクション制御も、二重送信防止のロック機構も組んでいない。ただの`db.post.create`が、無防備なままインターネットに晒されていたのだ。あの朝の冷や汗と、SQLを叩いて重複データを手作業で削除した虚しさは、今でも夢に出る。
Server Actionsは「魔法」ではない。単なる「サーバー関数をリモート呼び出しするRPC」に過ぎない。この本質を履き違えると、僕らのプロダクトは簡単に崩壊する。
1. 状態管理の「末端」を制する者がUXを制す
Server Actionsの最大の罠は「裏でよしなにやってくれる感」だ。API Routeなら「`loading`状態を管理してボタンを無効化する」という意識が働くが、Server Actionsだとついそのケアを忘れる。
ここで多くの人が`useState`を乱用し、コンポーネントツリー全体を再レンダリングさせてLighthouseのスコアをドブに捨てる。僕もその一人だった。解決策は、React 19以降の標準である`useFormStatus`に尽きる。
'use client'
import { useFormStatus } from 'react-dom'
// フォームの「送信中」状態を親から切り離す
export function SubmitButton() {
const { pending } = useFormStatus()
// pendingはフォーム送信時のみtrueになる。
// これにより、フォーム以外の要素を再レンダリングさせずにボタンだけを制御できる。
return (
<button type="submit" disabled={pending} className="transition-opacity">
{pending ? '送信中...' : '投稿する'}
</button>
)
}
なぜ`useState`ではなく`useFormStatus`なのか。それは「再レンダリングの範囲」が圧倒的に狭いからだ。`useState`で親コンポーネントの状態を管理すると、入力のたびにツリー全体が再評価される。`useFormStatus`はフォームの直下で完結するため、パフォーマンスのボトルネックを物理的に排除できる。
2. バリデーションエラーは「例外」ではない
Zodでバリデーションを組み、エラーを`throw`で投げようとしていないか? それは地獄への入り口だ。
Next.jsの`error.tsx`(Error Boundary)は、アプリケーションの致命的なクラッシュをキャッチするためのもの。バリデーションエラーのような「ユーザーの入力ミス」で画面を真っ白にしていいはずがない。エラーは例外ではなく「データ」として返す。これが鉄則だ。
| アプローチ | 手法 | メリット | デメリット |
|---|---|---|---|
| 例外処理 | `throw new Error()` | 実装が楽 | UXが最悪(画面が切り替わる) |
| データ返却 | `return { error: ... }` | 制御が容易 | 戻り値の型定義が少し煩雑 |
今の僕の最適解はこれだ。
'use server'
export async function createPost(prevState: any, formData: FormData) {
const result = schema.safeParse(Object.fromEntries(formData))
// バリデーションエラーを「データ」として返す
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors
}
}
try {
await db.post.create({ data: result.data })
return { success: true }
} catch (e) {
return { success: false, message: 'DB接続エラーが発生しました' }
}
}
3. 「API Routeに逃げる」勇気を持つ
「全部Server Actionsで書く」という思想に固執するのは、エンジニアとしての思考停止だ。境界線を見極める必要がある。
API Routeを検討すべきケース
- 冪等性(Idempotency)の担保が困難な外部連携: 決済APIなど、二重送信が致命的になるケース。
- 長時間実行されるタスク: Server ActionsはHTTPリクエストと密結合しているため、タイムアウトのリスクがある。
- サーバーサイドの複雑なステートマシン: 状態遷移が複雑なロジックは、専用のエンドポイントで疎結合に管理すべき。
結論:コードは「数ヶ月後の自分」への手紙だ
結局、設計とは「どこにロジックを配置するか」というトレードオフの連続だ。
1. データ取得はServer Components: キャッシュ戦略(`revalidateTag`等)をフル活用する。
2. 書き込みはServer Actions + `useFormState`: 状態を末端に閉じ込め、UXを担保する。
3. 複雑なロジックはAPI Route: 境界を明確にし、保守性を確保する。
Server Actionsは強力だ。だからこそ、自分の技術力で制御できる範囲を見極めなければならない。コードは、数ヶ月後の自分が読み返した時に「あ、こいつちゃんと考えてたな」と思えるものでなければならない。
爆速開発の代償として「負債」を積み上げるのはもうやめよう。魔法に頼らず、泥臭く、しかし洗練された設計を積み重ねる。それが、今の個人開発における「最適解」だ。