kt-tech.blog

画像

【実装】Next.js Server Actionsの実践パターン集

Share
💡
Next.js App RouterのServer Actionsを実際のチャットアプリで使ったパターンをまとめました。Route Handlersとの使い分けも解説します。

はじめに

Server Actions(“use server”)はNext.js App Routerで、クライアントからサーバー側の関数を直接呼び出せる仕組みです。Route Handlersと比較して、API設計やfetchラッパーが不要で、型安全に呼び出せるのが特徴です。

Server Actions vs Route Handlers

観点 Server Actions Route Handlers
型安全 ✅ 自動(関数の引数/戻り値がそのまま) ❌ 手動でrequest/response型定義
エンドポイント設計 不要 必要(/api/xxx)
外部からのアクセス ❌ できない ✅ Webhook等で使える
Edge Runtime △ 制限あり ✅ 対応
キャッシュ なし Cache-Control設定可
💡
内部のCRUD操作にはServer Actions、外部連携(Webhook受信等)にはRoute Handlersと使い分けるのが良い。

パターン1: CRUD操作

// services/character.ts
"use server";

import prisma from "@/lib/prisma";

export async function getCharacters(userId?: string) {
  return prisma.character.findMany({
    where: {
      OR: [{ userId: null }, ...(userId ? [{ userId }] : [])],
    },
    orderBy: { createdAt: "asc" },
  });
}

export async function getCharacterBySlug(slug: string) {
  return prisma.character.findUnique({ where: { slug } });
}

シンプルなDB操作をそのまま関数として公開。クライアントからimportして呼ぶだけで型が通ります。

パターン2: getOrCreate(べき等操作)

// services/chatHistory.ts
"use server";

export async function getOrCreateTodaySession(
  userId: string,
  characterId: string,
) {
  const date = todayDate();
  const existing = await prisma.chatSession.findUnique({
    where: { userId_characterId_date: { userId, characterId, date } },
    include: { messages: { orderBy: { createdAt: "asc" } } },
  });
  if (existing) return existing;

  return prisma.chatSession.create({
    data: { userId, characterId, date },
    include: { messages: { orderBy: { createdAt: "asc" } } },
  });
}

パターン3: TanStack Queryとの組み合わせ

// Server ActionをqueryFnとして直接使用
const { data: characters } = useQuery({
  queryKey: ["characters", userId],
  queryFn: () => getCharacters(userId),  // Server Actionを直接渡す
  enabled: !!userId,
});

Server Actionsはただの非同期関数なので、TanStack QueryのqueryFnにそのまま渡せます。fetch URLの管理が不要で、戻り値の型も自動推論されます。

パターン4: 引数にユニオン型

export async function saveMessage(
  sessionId: string,
  role: "user" | "assistant",  // stringではなくユニオン型で制約
  content: string,
) {
  return prisma.chatMessage.create({
    data: { sessionId, role, content },
  });
}

Route HandlersだとリクエストボディのバリデーションにZod等が必要ですが、Server Actionsなら引数の型がそのまま制約になります。

まとめ

  • 内部CRUD → Server Actions、外部連携 → Route Handlers

  • TanStack QueryのqueryFnにServer Actionをそのまま渡せる

  • 引数のユニオン型でRoute Handlersのバリデーション相当の安全性を確保