Next.js (App Router) + Server Actions:API Routeの呪縛を解き、開発体験を極限まで高める

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 ActionsUIとの密接な連携が求められるため
Webhooksの受信API Route外部からのPOSTリクエストを受ける必要があるため
外部サービスへの公開APIAPI Route認証やレート制限を外部向けに設計する必要があるため
ファイルダウンロード等API Routeストリームレスポンスの制御が容易なため

5. 結論:ツールに振り回されるな

Server Actionsは、複雑なWeb開発の現場で「本質」に集中するための武器だ。API Routeという中継地点を省略することで、コードの記述量は減り、型安全性の維持コストも下がる。

しかし、技術の流行に飛びつく前に思い出してほしい。「疎結合を保つ設計」と「キャッシュの深い理解」を怠れば、どんな最新技術もただの「不安定な負債」に成り下がる。

API Routeの呪縛から解放される準備はできたか。まずは、小さく一つ、サービス層を切り出し、Server Actionsへ移行することから始めてみてほしい。その先には、今までとは別次元の、圧倒的な開発体験が待っているはずだ。