Next.js Server Actions:API Routesを過去にするための「境界線」の設計術

Next.js Server Actions:API Routesを過去にするための「境界線」の設計術

「API Routesを叩くために、わざわざ型を共有して、エンドポイントを定義して…」。
ふとコードを見返した時、この無駄な儀式にどれほどの時間を溶かしているのかと絶望したことはないだろうか。

Next.jsのApp Routerが登場して久しいが、いまだに「フロントとバックを分離する」という強迫観念に縛られている現場は多い。だが、Server Actionsは単なる「APIの代替」ではない。クライアントとサーバーの境界を溶かし、型安全なRPCを直書きするための最強の武器だ。

今回は、私がプロダクト開発で泥をすすりながら学んだ、「Server Actionsで事故らず、かつ爆速で実装する」ための実践的な設計論を共有する。


1. 認証は「ラップ」して強制する

「Server Actionsに認証を書き忘れる」というミスは、もはや事故ではなく設計の怠慢だ。個別の関数に認証ロジックをコピペするのは二度とやめよう。

認証済みのユーザーコンテキストを強制するHigher-Order Function(HOF)を導入するのが、現場の最適解だ。

// lib/safe-action.ts
import { auth } from "@/auth";

export async function authenticatedAction<T>(
  action: (data: T, userId: string) => Promise<any>
) {
  return async (data: T) => {
    const session = await auth();
    if (!session?.user?.id) throw new Error("Unauthorized");
    
    return action(data, session.user.id);
  };
}

// 使い方
export const updateProfile = authenticatedAction(async (data: ProfileData, userId: string) => {
  return await db.user.update({ where: { id: userId }, data });
});

こうすれば、開発者は認証ロジックを意識せず、ビジネスロジックに集中できる。型定義も維持されるため、IDEの補完も完璧だ。


2. キャッシュ戦略:なぜ「全消し」は破綻するのか

キャッシュの罠にハマった経験は誰にでもあるはずだ。「とりあえず`revalidatePath('/')`しておけばいいか」という安易な実装は、アプリが肥大化した瞬間に牙を剥く。

私が過去に直面したのは、「タグ付けの粒度が荒すぎて、無関係なページまで再検証が走る」というパフォーマンスの悪化だった。

キャッシュ設計の鉄則

1. パスベースよりタグベース: `revalidatePath`は最終手段。特定のデータ更新には`revalidateTag`を使い、ドメイン単位でタグを命名する。 2. 命名規則の厳格化: `user:${userId}:profile` のように、階層とIDを含めたタグを徹底する。

// データの取得側
const user = await fetch(url, { next: { tags: [`user:${userId}:profile`] } });

// 更新側
export async function updateUsername(username: string) {
  // ...DB更新
  revalidateTag(`user:${userId}:profile`);
}

この命名規則を守るだけで、無駄な再レンダリングは劇的に減る。キャッシュは「なんとなく」管理するものではなく、データ構造の延長として設計すべきだ。


3. UXの極み:`useOptimistic`でラグを消し去る

サーバーレス環境において、ネットワークの遅延は避けられない。ボタンを押してから0.5秒のラグがあるだけで、ユーザーは「アプリが重い」と感じる。

`useOptimistic`を使えば、サーバーのレスポンスを待たずにUIを先行更新できる。

'use client'

import { useOptimistic } from 'react';

export function TodoList({ initialTodos }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    initialTodos,
    (state, newTodo) => [...state, { title: newTodo, pending: true }]
  );

  async function action(formData: FormData) {
    const title = formData.get('title');
    addOptimisticTodo(title); // 即座にUI更新
    await createTodo(title);  // サーバー処理
  }
  
  // ...以下レンダリング
}

この「先行更新」があるかどうかで、プロダクトの品格が決まる。サーバーの処理結果を待つのではなく、「成功した前提でUIを動かす」のが、モダンなフロントエンドの流儀だ。


結論:APIという「壁」を壊せ

Server Actionsを採用することは、単にコード量を減らすことではない。フロントエンドとバックエンドという分断された思考を捨て、「ユーザーに価値を届けるための単一のロジック」として捉え直すことだ。

  • 型安全を妥協しない(Zodでのバリデーションは必須)
  • 認証をHOFで強制する
  • キャッシュの粒度をデータ構造と一致させる
  • 楽観的UIでラグを殺す
これらを徹底すれば、API Routesをせっせと書く時代にはもう戻れなくなるはずだ。さあ、境界線を溶かし、より本質的な開発に没頭しよう。