20260731 095613 next.js (app router) + server actionsによるフルスタック開発の最適解

「API Routeを書いてる奴、まだ2010年代の亡霊に取り憑かれてるの?」

Next.jsのApp Routerが登場して久しいが、いまだに律儀に`/api`ディレクトリを掘って、`fetch`を叩いて、JSONをパースして……なんて泥臭いことをやってるなら、今すぐその手を止めろ。Server Actionsは単なる「便利な機能」じゃない。RESTという呪縛から解放された、RPC(Remote Procedure Call)の現代的再実装だ。フロントエンドとバックエンドの境界線を物理的に消し飛ばす、この破壊的なパラダイムシフトに乗れないエンジニアは、これから確実に淘汰される。

1. Server Actionsは「楽」だが、死ぬほど「罠」だらけ

Server Actionsは一見すると魔法だ。Zodで型を縛り、そのままDBを叩く。これで十分シンプルに見える。だが、多くのエンジニアがここで「再検証(revalidatePath)のタイミング」で詰み、深夜3時に頭を抱えることになる。

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

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

// 戻り値の型を統一する。これがないとフロント側での型推論が死ぬ
export type ActionResponse<T> = 
  | { success: true; data: T }
  | { success: false; error: string; fieldErrors?: Record<string, string[]> };

const schema = z.object({
  username: z.string().min(3),
  email: z.string().email(),
});

export async function createUser(prevState: any, formData: FormData): Promise<ActionResponse<{ id: string }>> {
  const result = schema.safeParse(Object.fromEntries(formData));

  if (!result.success) {
    return { success: false, error: "バリデーションエラー", fieldErrors: result.error.flatten().fieldErrors };
  }

  try {
    const user = await db.user.create({ data: result.data });
    revalidatePath("/dashboard");
    return { success: true, data: { id: user.id } };
  } catch (e) {
    // 実際にはここでSentry等にログを飛ばすのがプロの流儀
    console.error("DB Error:", e);
    return { success: false, error: "サーバー内部で予期せぬエラーが発生しました" };
  }
}

2. キャッシュ地獄から脱出する「楽観的UI」の鉄則

以前、タスク管理アプリを構築した際、Server Actionsを実行した直後にUIが更新されないバグに3時間溶かした。原因は`revalidatePath`への過信だ。ネットワークのラグやキャッシュ戦略の裏をかかれ、ユーザーの目には「何も変わっていない画面」が映り続ける。

解決策は、`useOptimistic`による強引なUI反映だ。

// app/components/TodoForm.tsx
"use client";

import { useOptimistic } from "react";
import { addTodo } from "@/actions/todo";

export function TodoForm({ initialTodos }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    initialTodos,
    (state, newTodo) => [...state, { ...newTodo, id: Math.random().toString() }]
  );

  async function action(formData: FormData) {
    addOptimisticTodo({ title: formData.get("title") });
    const res = await addTodo(formData);
    if (!res.success) alert(res.error);
  }

  return <form action={action}>...</form>;
}

これを実装しないServer Actionsは「レスポンスが遅いクソアプリ」だ。UXを語るなら、ここを避けて通るな。

3. なぜ「API Route」への執着を捨てるべきか

「フロントとバックを疎結合にしたい」という幻想を捨てろ。GoogleやNetflixのような超巨大組織ならともかく、個人開発でAPIの型定義を別パッケージに切り出し、`trpc`や`OpenAPI`で整合性を保つ作業に費やす時間は、現代のエンジニアリングにおいて純粋な負債でしかない。

Server Actionsの真価は、DBスキーマ変更がUIに即座にコンパイルエラーとして伝播する「単一ソースコードの恩恵」にある。このスピード感に慣れると、もうREST APIなんて書く気になれないはずだ。

4. 限界を認め、プロとして立ち回れ

もちろん、Server Actionsは万能ではない。以下の現実と向き合う覚悟が必要だ。

  • デバッグの闇: サーバーサイドのスタックトレースを追うには、`instrumentation.ts`を活用し、ログを構造化せよ。`console.log`で凌ぐのは学生レベルだ。
  • エラーハンドリングの設計: 前述した`ActionResponse`のように、戻り値を型で縛り、クライアント側で網羅的な分岐を強制せよ。
  • API Routeが必要なケース: 認証(Middlewareとの兼ね合い)や、Stripe等の外部サービスからのWebHook受信、あるいはモバイルアプリへのAPI提供など、「ブラウザ外からのアクセス」が必要な場合は迷わずAPI Routeを書け。それ以外はServer Actionsで十分だ。

結論:APIを捨てる覚悟を持て

結局のところ、Server Actionsは「フロントエンドエンジニアがバックエンドを触るための最適解」であり、「バックエンドエンジニアがフロントの面倒を見るための最適解」だ。

API Routeを書いていた時間を、UIの磨き込みやロジックのブラッシュアップに回せ。技術スタックを統合し、コンテキストスイッチを極限まで減らすこと。それこそが、個人の開発スピードを爆速にする唯一の道だ。

Next.jsの機能を使い倒せ。APIなんて、どうしても必要な時以外は書くな。