API Routeを捨ててServer Actionsへ。Next.js開発を「脱・冗長」する最適解

API Routeを捨ててServer Actionsへ。Next.js開発を「脱・冗長」する最適解

「APIエンドポイントを作って、型定義を共有して、`fetch`を叩く」。
この儀式、正直もう飽き飽きしてないか?

かつての僕もそうだった。フロントとバックエンドを律儀に分離し、エンドポイント名に悩み、`schema.ts`を両方のディレクトリにコピーしては同期ズレに泣く。そんな「化石のような開発」を続けていたせいで、どれほどの時間を浪費したことか。

Next.jsのApp Routerが登場して久しいが、未だにこの古いパラダイムから抜け出せないエンジニアが多い。Server Actionsは単なる「便利な機能」じゃない。フロントとバックエンドの境界線を消滅させ、開発体験を根本から覆すための武器だ。

今回は、現場で泥臭くコードを書き、何度もキャッシュの罠にハマり、徹夜でデバッグした末に見つけた「Server Actionsで勝ち抜くための設計論」を叩き込む。


1. なぜ「API Route」は現代の足枷になるのか

API Routeが非効率な理由は明確だ。「型定義の二重管理コスト」と「ネットワークオーバーヘッド」だ。

  • 型管理の地獄: APIのレスポンス型をフロントで定義し直す。バックエンドの変更がフロントに伝播せず、型安全が崩壊する。
  • オーバーヘッド: クライアントからわざわざHTTPリクエストを飛ばし、サーバー側でパースし、レスポンスをJSON化して返す。これ、内部関数呼び出しならミリ秒で終わる話だ。
Server Actionsなら、クライアントから直接サーバー側の関数を呼び出せる。型はTypeScriptが自動で推論し、ネットワークリクエストの複雑な管理はNext.jsが裏で引き受けてくれる。

// app/actions/post.ts
'use server'

import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'

export async function createPost(prevState: any, formData: FormData) {
  const title = formData.get('title') as string
  
  // バリデーションをサボるな。ここが守りの要だ
  if (!title) return { error: 'タイトルは必須だ。適当にやるなよ。' }

  try {
    await db.post.create({ data: { title } })
    revalidatePath('/posts')
    return { success: true }
  } catch (e) {
    return { error: 'サーバーで何か起きた。ログを見ろ。' }
  }
}


2. 認証統合でハマる「キャッシュの闇」

Auth.js (NextAuth) を使っているなら要注意だ。Server Actions内で`auth()`を呼ぶと、往々にして「ログアウトしたのにセッション情報が残る」という現象に遭遇する。

原因はNext.jsのData Cacheだ。Server Actionsも内部的にはキャッシュの対象になり得るため、セッション情報が古いまま固定されることがある。

解決策:明示的な無効化と、実行タイミングの制御

import { auth } from '@/auth'
import { revalidatePath } from 'next/cache'

export async function deletePost(id: string) {
  const session = await auth()
  if (!session) throw new Error('権限なし')

  await db.post.delete({ where: { id } })
  
  // 削除後はキャッシュをクリアし、最新の状態を強制取得させる
  revalidatePath('/posts')
}

この「キャッシュをどう制御するか」という感覚こそ、今のNext.js開発における最大のスキルセットだ。ここを理解せず「なんとなく動く」で済ませていると、半年後に大規模なセキュリティホールや整合性エラーとして自分に跳ね返ってくる。


3. 型安全の幻想をぶち壊す「バリデーション」

「Server Actionsは型安全だから安心」なんて言っている奴は危うい。クライアントから送られてくる`FormData`は、依然として「型のないただの箱」だ。

Zodによる型変換の強制こそが、メンテ可能なコードへの唯一の道だ。

import { z } from 'zod'

const PostSchema = z.object({
  title: z.string().min(1).max(100),
})

export async function createPost(prevState: any, formData: FormData) {
  const result = PostSchema.safeParse(Object.fromEntries(formData))
  
  if (!result.success) {
    return { error: result.error.flatten().fieldErrors }
  }
  
  // バリデーション通過後は、result.dataで型安全に突き進める
  const { title } = result.data
  // ...DB操作
}


4. まとめに:使い分ける判断基準

Server Actionsは最強だが、万能ではない。以下の表を指針にしてくれ。

シナリオ推奨アプローチ理由
フォーム送信・データ更新Server Actions状態の再検証(revalidate)が容易で、UXが向上する。
高頻度なデータ取得Server Componentsキャッシュ制御が最も洗練されており、最速。
外部からのAPIリクエストRoute HandlersWebhook受け取りなど、クライアント以外との通信用。
複雑なロジックの分離tRPC / 外部APIフロントエンドとサーバーが完全に独立したチーム体制なら。

最後に

技術は道具だ。流行っているから使うのではなく、「なぜこれを使うのか」を自分の言葉で説明できるようになれ。

Server Actionsを使いこなすことは、単にコードを短くすることじゃない。「複雑さをどこに閉じ込めるか」というアーキテクチャの主導権を、自分の中に取り戻すことだ。

さあ、古いAPI Routeの設計書はゴミ箱に捨てて、もっと泥臭く、もっとモダンなコードを書こうぜ。半年後の自分に感謝されるような、強固なコードを。