Next.js (App Router) + Server Actions:API Routeを捨てて「境界線」を溶かす開発へ
「API Routeを書いていたあの時間は、結局なんだったのか」。
初めてServer Actionsを触り、フロントとバックの境界が霧散する感覚を味わったとき、脳裏をよぎったのはそんな虚無感だった。これまでせっせと型定義を共有し、エンドポイントを叩き、ローディング状態と戦い、SWRやReact Queryでキャッシュをこねくり回していたあの苦労。
結論から言おう。今のNext.jsでAPI Routeを量産するのは、よほど特殊なマイクロサービス構成でもない限り、ただの「苦行」だ。Server Actionsは単なる便利機能ではない。フロントエンドとバックエンドを一つの関数呼び出しへと昇華させる、パラダイムシフトそのものだ。
1. なぜAPI Routeは「過去の遺物」になったのか
かつての開発フローを思い出してほしい。
1. `pages/api` にエンドポイントを生やす
2. `fetch` を書き、型を合わせるためにインターフェースを定義する
3. クライアント側で `useEffect` やライブラリを使い、リクエストのライフサイクルを管理する
これらすべてが、「ネットワーク越しにデータをやり取りする」という物理的な制約を、コード上で無理やり解決するための儀式だった。Server Actionsは、このネットワーク越しという意識を抽象化し、単なる「関数の実行」へと置き換える。
厳密な型安全性を担保する実装
ZodとTypeScriptを組み合わせれば、バックエンドの型安全性をクライアントまで透過的に持ち込める。`any` を使うような甘えは捨てよう。"use server";
import { z } from "zod";
import { db } from "@/lib/db";
import { revalidatePath, revalidateTag } from "next/cache";
const UserSchema = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
});
// Zodから推論して型を完璧に制御する
type UserInput = z.infer<typeof UserSchema>;
export async function createUser(prevState: unknown, formData: FormData) {
const result = UserSchema.safeParse(Object.fromEntries(formData));
if (!result.success) {
return { error: result.error.flatten().fieldErrors };
}
try {
await db.user.create({ data: result.data });
// 特定のタグを指定してキャッシュをパージする「実践的」な手法
revalidateTag("users");
return { success: true };
} catch (e) {
return { error: "DB接続エラー" };
}
}
2. Server Actionsの「罠」と向き合う
Server Actionsは魔法ではない。特に「キャッシュ」と「シリアライズ」の制限は、多くのエンジニアがモニターを叩き割りたくなるポイントだ。
キャッシュの制御:revalidateTagの使い所
`revalidatePath` は便利だが、複雑なアプリでは制御が効かなくなる。特定のデータが更新された時だけピンポイントでキャッシュを捨てるなら、`revalidateTag` が必須だ。- revalidatePath: パス単位でキャッシュを再検証する(大雑把な更新に)
- revalidateTag: データキャッシュ単位で再検証する(より細かな制御に)
シリアライズの限界を知る
Server Actionsは、引数と戻り値に「シリアライズ可能なデータ(JSON)」しか扱えない。`Date` オブジェクトや `Map`、あるいは複雑なクラスインスタンスを直接渡そうとすると、容赦なくエラーが飛んでくる。「何でもサーバーに投げられる」と勘違いせず、「プリミティブな値とJSONのみをやり取りする」というWebの原点に立ち返ることが、バグを減らす最短距離だ。
3. 「守護神」としての認証ラッパー
Server Actionsを単なる「コンポーネントの延長」だと勘違いしてはいけない。これは紛れもなく公開されたAPIエンドポイントだ。
認証チェックを個々の関数に書くのはDRY原則に反する。高階関数で「守護神」を作れ。
type ActionFn<T> = (user: User) => Promise<T>;
export async function withAuth<T>(action: ActionFn<T>): Promise<T> {
const user = await getAuthUser();
if (!user) throw new Error("Unauthorized");
return action(user);
}
// 使うときはこれだけ
export const updateProfile = async (data: any) => withAuth(async (user) => {
// ここで安全にDB操作
});
4. プロフェッショナルとしての現実解
Server Actionsは強力だが、銀の弾丸ではない。
- 小〜中規模プロダクト: Server Actionsで爆速開発するのが正解。`eslint-plugin-react-server-components` を導入し、`"use client"` と `"use server"` の境界を静的解析で守るのがチーム開発の鉄則だ。
- 超大規模・複雑なマイクロサービス: 依然として独立したAPIサーバー(またはtRPC等の型共有基盤)に軍配が上がる。Server Actionsは結合度が強すぎるため、責務が肥大化するとメンテナンスが困難になる。
最後に
Server Actionsを使いこなすことは、「ネットワーク越し」の複雑性を隠蔽し、ドメインロジックそのものに集中することだ。API Routeをせっせと量産していたあの頃の自分に言いたい。「お前が書いていたそのコード、全部捨てて、境界線を溶かせ」と。技術的負債を積み上げる前に、まずは小さく、この「体験」を導入してみてほしい。その先には、全く新しいNext.jsの景色が待っている。