もうAPIは書くな — Next.js 16のServer Actionsで開発の常識が完全に壊れた

$ |
もうAPIは書くな — Next.js 16のServer Actionsで開発の常識が完全に壊れた
$

2025年末にリリースされたNext.js 16は、フルスタック開発のあり方を根本から変えた。特にServer Actionsの進化は、フロントエンドとバックエンドの境界線を曖昧にし、開発者の生産性を劇的に向上させている。本記事では、Next.js 16のServer Actionsを中心に、App Router、React Server Components(RSC)との連携を実践的に解説する。

App Routerの成熟 — もうPages Routerには戻れない

Next.js 13で導入されたApp Routerは、バージョンを重ねるごとに安定性を増してきた。Next.js 16では、キャッシュの挙動が大幅に改善され、開発者を悩ませてきた「キャッシュが効きすぎる」問題がようやく解決された。

App Routerの根幹にあるのは、React Server Components(RSC)だ。コンポーネントがサーバー側で実行されることで、データベースへの直接アクセスやファイルシステム操作がコンポーネント内で完結する。クライアントにはレンダリング済みのHTMLだけが送られるため、バンドルサイズの削減にも直結する。

// app/users/page.tsx — Server Component
import { prisma } from '@/lib/prisma'

export default async function UsersPage() {
  const users = await prisma.user.findMany({
    orderBy: { createdAt: 'desc' },
    take: 20,
  })

  return (
    <div>
      <h1>ユーザー一覧</h1>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  )
}

上記のコードを見て驚く人もいるだろう。APIルートもuseEffectも不要。コンポーネントの中で直接Prismaを呼び出し、データを取得してレンダリングしている。これがRSCの力だ。

Server Actions — フォーム処理の革命

Server Actionsは、Next.js 14で安定版になった機能だが、Next.js 16でその真価が発揮された。従来のWeb開発では、フォーム送信の処理に以下のステップが必要だった。

  • クライアント側でフォームデータを収集
  • fetch()でAPIエンドポイントにPOSTリクエスト
  • サーバー側のAPIルートでリクエストを解析
  • バリデーション、DB操作、レスポンス返却
  • クライアント側でレスポンスを処理

Server Actionsでは、このフローが劇的に簡素化される。

// actions/create-user.ts
'use server'

import { z } from 'zod'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'

const schema = z.object({
  name: z.string().min(1, '名前は必須です'),
  email: z.string().email('有効なメールアドレスを入力してください'),
})

export async function createUser(formData: FormData) {
  const parsed = schema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
  })

  if (!parsed.success) {
    return { error: parsed.error.flatten().fieldErrors }
  }

  await prisma.user.create({
    data: parsed.data,
  })

  revalidatePath('/users')
  return { success: true }
}
// components/create-user-form.tsx
'use client'

import { createUser } from '@/actions/create-user'
import { useActionState } from 'react'

export function CreateUserForm() {
  const [state, action, isPending] = useActionState(createUser, null)

  return (
    <form action={action}>
      <input name="name" placeholder="名前" />
      {state?.error?.name && <p>{state.error.name}</p>}

      <input name="email" placeholder="メール" />
      {state?.error?.email && <p>{state.error.email}</p>}

      <button disabled={isPending}>
        {isPending ? '送信中...' : '登録'}
      </button>
    </form>
  )
}

ActionResult パターン — 型安全なエラーハンドリング

実務でServer Actionsを使い込むと、エラーハンドリングの一貫性が課題になる。我々のチームでは「ActionResult パターン」を採用している。

// lib/action-result.ts
type ActionResult<T> =
  | { success: true; data: T }
  | { success: false; error: string }

export function ok<T>(data: T): ActionResult<T> {
  return { success: true, data }
}

export function err<T>(error: string): ActionResult<T> {
  return { success: false, error }
}

このパターンにより、すべてのServer Actionが同じ形式でレスポンスを返す。呼び出し側はresult.successで分岐するだけでよい。TypeScriptの型推論も完璧に機能し、success: trueの場合はdataに、success: falseの場合はerrorにアクセスできる。

React 19の useActionState と useOptimistic

React 19で導入されたuseActionStateフックは、Server Actionsとの相性が抜群だ。従来のuseFormStateを置き換えるこのフックは、送信状態(pending)の管理が組み込まれている。

さらに、useOptimisticフックを組み合わせることで、サーバーレスポンスを待たずにUIを即座に更新できる。SNSの「いいね」ボタンのような機能を実装する場合、ユーザーがボタンを押した瞬間にカウントが増え、サーバー側の処理が完了したら実際の値に置き換わる。

// Optimistic Update の例
'use client'

import { useOptimistic } from 'react'
import { toggleLike } from '@/actions/toggle-like'

export function LikeButton({ postId, likes, isLiked }) {
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(
    { count: likes, liked: isLiked },
    (current, _) => ({
      count: current.liked ? current.count - 1 : current.count + 1,
      liked: !current.liked,
    })
  )

  return (
    <form action={async () => {
      setOptimisticLikes(null)
      await toggleLike(postId)
    }}>
      <button>
        {optimisticLikes.liked ? '❤️' : '🖤'} {optimisticLikes.count}
      </button>
    </form>
  )
}

Parallel RoutesとIntercepting Routes

App Routerの強力な機能であるParallel Routesは、同一レイアウト内で複数のページを同時にレンダリングできる。ダッシュボードのような複雑なUIで威力を発揮する。

例えば、管理画面で「売上サマリー」「最新注文」「アクティブユーザー」を並列に読み込む場合、従来は1つのページコンポーネント内でPromise.allを使うか、個別にローディング状態を管理する必要があった。Parallel Routesなら、各スロットが独立してストリーミングされる。

// app/(admin)/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  sales,      // @sales スロット
  orders,     // @orders スロット
  users,      // @users スロット
}: {
  children: React.ReactNode
  sales: React.ReactNode
  orders: React.ReactNode
  users: React.ReactNode
}) {
  return (
    <div className="grid grid-cols-3 gap-4">
      {sales}
      {orders}
      {users}
    </div>
  )
}

Server Actionsのセキュリティ考慮

Server Actionsは便利だが、セキュリティへの配慮は不可欠だ。Server Actionは実質的にPOSTエンドポイントとして公開されるため、以下の点に注意が必要だ。

  • 認証チェック: すべてのServer Action内で認証状態を確認する。「ログインしているはず」という前提は禁物だ
  • 認可チェック: リソースへのアクセス権限を必ず検証する。他人のデータを操作できないことを担保する
  • 入力バリデーション: Zodなどのライブラリでサーバー側でも必ずバリデーションを行う。クライアント側のバリデーションだけでは不十分だ
  • レート制限: 必要に応じてレート制限を実装する。upstashのRatelimitなどが便利

まとめ — フルスタックTypeScriptの到達点

Next.js 16のServer Actionsは、フルスタック開発の常識を変えた。APIルートを書く手間が減り、型安全なデータフローがフロントエンドからバックエンドまで一貫する。React 19のuseActionState、useOptimisticとの組み合わせは、UXの向上にも直結する。

もちろん、すべてのプロジェクトにNext.jsが最適とは限らない。しかし、TypeScriptでフルスタック開発を行うなら、2026年現在、Next.js 16は最も生産性の高い選択肢だと断言できる。

「フレームワークは道具であり、目的ではない。しかし、良い道具は確実に開発者の可能性を広げる。」

次回は、バックエンド言語の選定について、RustとGoの比較を行う。フロントエンドがNext.jsで固まったとして、バックエンドマイクロサービスにはどちらが適しているのか。データに基づいて検証する。