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()`を利用した認証ガードだ。これはリクエストスコープでキャッシュされるため、同一リクエスト内での複数呼び出しがオーバーヘッドにならない。
| 比較項目 | getServerSession | auth() (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の呪縛から自分を解き放ってみないか?