Next.js (App Router) + Server Actions:API Routeという「過去の遺物」を捨て去るための全記録

Next.js (App Router) + Server Actions:API Routeという「過去の遺物」を捨て去るための全記録

深夜2時。画面に映るのは、エンドポイントごとにバラバラに定義されたAPI Routeと、それらを叩くための冗長な`fetch`ラッパー。型定義を共有するために`shared`ディレクトリを往復し、バックエンドのバリデーションとフロントのバリデーションを二重管理する。

「俺はソフトウェアエンジニアなのか、それともただの『データの運び屋』なのか?」

API Routeという中間層に依存しきった開発は、もはや負け戦だ。Next.jsのApp RouterとServer Actionsを使いこなせば、フロントとバックの境界は極限まで溶ける。今回は、綺麗事抜きの「現場の最適解」を共有する。


1. API Routeを捨てるという決断:型安全は「場所」で作る

API Routeを捨てる最大のメリットは、「型定義の共有問題」が物理的に消滅することだ。Zodでスキーマを定義し、それをそのままServer Actionに流し込む。これで、クライアントとサーバーの間にあった「型情報の同期ずれ」という悪夢から解放される。

// actions/user.ts
"use server";

import { z } from "zod";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";

const UserSchema = z.object({
  name: z.string().min(1, "名前は必須です"),
  email: z.string().email("無効なメールアドレスです"),
});

export async function updateUser(prevState: any, formData: FormData) {
  const result = UserSchema.safeParse(Object.fromEntries(formData));
  
  if (!result.success) {
    return { error: result.error.flatten().fieldErrors };
  }

  try {
    await db.user.update({
      where: { id: "current-user-id" },
      data: result.data,
    });
    return { success: true };
  } catch (err) {
    console.error("Database update failed:", err);
    return { error: { _form: ["サーバーサイドでの更新に失敗しました"] } };
  }
}

  • アンチパターン: 複雑なビジネスロジックをすべてAction内に詰め込むこと。Actionはあくまで「コントローラー」であり、ドメインロジックは別層(Service層など)に切り出すのが、長期的なメンテの鉄則だ。

2. 再レンダリングの罠:`revalidatePath`の乱用は「死」を招く

かつて俺は、`revalidatePath`を連打して全キャッシュをパージしていた。その結果、データ量が増えるたびに画面がカクつき、UXが著しく低下した。

解決策は「粒度」を極限まで絞ることだ。`revalidatePath`ではなく、`revalidateTag`を活用して特定のデータキャッシュのみを無効化する。

// 特定のデータだけをピンポイントで更新する
import { revalidateTag } from "next/cache";

export async function updatePost(id: string, data: any) {
  // DB更新処理...
  revalidateTag(`post-${id}`); // このタグを持つキャッシュのみを捨てる
}

さらに、`useOptimistic`を活用し、サーバーレスポンスを待たずにUIを先行更新(楽観的更新)させる。これで「API待ちの体感時間」をゼロにできる。

  • アンチパターン: 頻繁に更新されるリストに対して、ページ全体のキャッシュをパージすること。これはパフォーマンスの自殺行為だ。

3. 認証の統合:`getServerSession`はなぜ重いのか

Next.jsの認証において、多くのエンジニアが`getServerSession`を多用するが、あれは内部で毎回セッションの検証とDBアクセスを走らせるため、大規模アプリケーションではレイテンシの要因になる。

現在、App Routerで推奨されるのは、`auth()`を利用した認証ガードだ。これはリクエストスコープでキャッシュされるため、同一リクエスト内での複数呼び出しがオーバーヘッドにならない。

比較項目getServerSessionauth() (Auth.js v5)
パフォーマンスセッション取得毎に検証リクエスト内キャッシュあり
型安全性弱い強い (TypeScriptネイティブ)
推奨度低い (レガシー)高い (モダン)

import { auth } from "@/auth";

export async function secureAction() {
  const session = await auth();
  if (!session) throw new Error("Unauthorized");
  
  // 認証済み処理...
}

  • アンチパターン: 認証が必要な全Actionに毎回セッション検証ロジックをコピペすること。共通のラッパー関数を作成し、責務を分離すべきだ。

まとめ:複雑なAPI層を捨てる勇気

Server Actionsは、ただの便利ツールではない。フロントエンドがインフラの制約から解放されるための革命だ。

1. バリデーションはZodで一元管理せよ。型安全は、境界線を跨ぐためのパスポートだ。
2. キャッシュはタグ付けで制御せよ。`revalidatePath`は「奥の手」として扱え。
3. 認証は`auth()`で統一せよ。パフォーマンスとセキュリティのバランスが取れる唯一の解だ。

API Routeを削除し、コードの行数が減るたびに、バグが入り込む余地が消えていく。この「削ぎ落とす快感」こそがフルスタック開発の醍醐味だ。

さて、コーヒーはもう飲み干した。あと一つだけ、このアーキテクチャで実装したい機能がある。お前も、API Routeの呪縛から自分を解き放ってみないか?