Next.js (App Router) + Server Actions:API Routeの呪縛を解き、開発体験を極限まで高める
「まだAPI Routeを書いているのか?」
ふと自分のリポジトリを見返した時、そう自問自答したことがないだろうか。Next.jsのApp Routerが普及して久しいが、未だに「API Routeの方が慣れている」「Server Actionsは不安定そう」という理由で、型定義の二重管理や無駄なfetch処理という技術的負債を積み上げているプロジェクトが多すぎる。
Server Actionsは、単なる便利な機能ではない。フロントエンドとバックエンドを「関数」という単位で直結させ、通信のオーバーヘッドと型安全性の欠如という、Web開発の長年の悩みを根底から覆すものだ。本稿では、Server Actionsを実戦投入する上で避けては通れない設計の勘所と、API Routeとの明確な棲み分けについて、私の苦い経験をもとに解説する。
1. なぜ「API Route」を捨てるべきなのか
従来のAPI Route開発には、常に「型安全の分断」というリスクが付きまとう。フロントとバックエンドで型を共有するために`trpc`や`zod`のスキーマを無理やり同期させ、CIで型チェックを走らせる。これらは確かに健全だが、本質的には「通信」という儀式を維持するためのコストだ。
Server Actionsを使えば、その儀式は消滅する。クライアントからサーバー上の関数を直接呼び出す。引数はシリアライズされ、戻り値は自動的に型推論される。この「直結」こそが、開発スピードを劇的に引き上げる鍵だ。
2. 実装のベストプラクティス:ドメインロジックの分離
Server Actionsに直接ビジネスロジックを書き殴るのは、プロジェクトの寿命を縮める行為だ。コンポーネントとロジックを密結合させないために、サービス層(Service Layer)を設ける構成を強く推奨する。
推奨ディレクトリ構成
src/
├── actions/ # UIのイベントハンドラ(Server Actions)
│ └── todo.ts
├── services/ # 純粋なビジネスロジック(DB操作等)
│ └── todo.service.ts
└── lib/ # 共通設定(Prisma, Zod, Auth.js)
実装例:サービス層を介したクリーンな実装
// services/todo.service.ts
import { prisma } from '@/lib/prisma';
export const createTodoItem = async (title: string) => {
// ここに複雑なバリデーションや外部連携をまとめる
return await prisma.todo.create({ data: { title } });
};
// actions/todo.ts
'use server'
import { createTodoItem } from '@/services/todo.service';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
const TodoSchema = z.object({ title: z.string().min(1).max(100) });
export async function createTodoAction(prevState: any, formData: FormData) {
const result = TodoSchema.safeParse(Object.fromEntries(formData));
if (!result.success) return { error: result.error.flatten().fieldErrors };
try {
await createTodoItem(result.data.title);
revalidatePath('/dashboard');
return { success: true };
} catch {
return { error: 'サーバー内部エラーが発生しました' };
}
}
この構成であれば、Server Actionsは「入力の受け取り」と「結果のハンドリング」に専念でき、ドメインロジックは単体テスト可能な純粋関数として切り出せる。
3. キャッシュの罠と「再検証」の深淵
Server Actionsで最も多くのエンジニアが躓くのが、データ更新後のキャッシュ管理だ。`revalidatePath` を適当に呼ぶだけでは、複雑なアプリケーションは支えきれない。
- `revalidatePath(path)`: 指定したパスのキャッシュを無効化する。基本はこれで十分だが、過剰な更新はパフォーマンスを低下させる。
- `revalidateTag(tag)`: 特定のデータソースに関連付けたタグを無効化する。`fetch`時に `next: { tags: ['todo'] }` を設定しておくことで、より粒度の細かい制御が可能になる。
- `unstable_cache`: 頻繁に呼び出される計算コストの高い関数にはこれを使う。DBへの問い合わせ頻度を劇的に下げられる。
4. API RouteとServer Actionsの明確な棲み分け
すべてをServer Actionsに置き換える必要はない。以下のケースでは、従来のAPI Routeやルートハンドラを選択すべきだ。
| ケース | 推奨 | 理由 |
|---|---|---|
| 画面からのユーザー操作 | Server Actions | UIとの密接な連携が求められるため |
| Webhooksの受信 | API Route | 外部からのPOSTリクエストを受ける必要があるため |
| 外部サービスへの公開API | API Route | 認証やレート制限を外部向けに設計する必要があるため |
| ファイルダウンロード等 | API Route | ストリームレスポンスの制御が容易なため |
5. 結論:ツールに振り回されるな
Server Actionsは、複雑なWeb開発の現場で「本質」に集中するための武器だ。API Routeという中継地点を省略することで、コードの記述量は減り、型安全性の維持コストも下がる。
しかし、技術の流行に飛びつく前に思い出してほしい。「疎結合を保つ設計」と「キャッシュの深い理解」を怠れば、どんな最新技術もただの「不安定な負債」に成り下がる。
API Routeの呪縛から解放される準備はできたか。まずは、小さく一つ、サービス層を切り出し、Server Actionsへ移行することから始めてみてほしい。その先には、今までとは別次元の、圧倒的な開発体験が待っているはずだ。