目次
Server Actionとは?Next.jsにおける新しいサーバー処理の仕組み
Next.jsでフォーム送信やデータ更新を実装する際、「API Routeを作って、fetchを書いて、状態管理をして…」という一連の流れに煩雑さを感じたことはありませんか?
Server Actionは、サーバー側で実行される非同期関数をクライアントから直接呼び出せる新機能です。Next.js 13.4以降で導入され、従来のAPI Routesを経由する開発フローを大きく簡素化します。
本記事では、Server Actionの基本概念から実際のメリット、注意点、具体的な実装方法まで、中小企業の開発現場で活用できる視点で解説します。限られたリソースで効率的な開発を実現したい方は、ぜひ参考にしてください。
Server Actionの基本概念
定義と実行の仕組み
Server Actionを一言で表すなら、**「フォームやボタンから直接サーバー処理を呼び出せる仕組み」**です。"use server"ディレクティブを使って定義し、以下のようなことが可能になります。
- フォームデータの送信と処理をAPI Route不要で実装
- データベースへの書き込みをコンポーネントから直接実行
- 外部APIの呼び出しをサーバー側で安全に処理
- ファイルアップロードや複雑なデータ操作
// Server Actionの基本例
'use server'
export async function createUser(formData) {
const name = formData.get('name')
const email = formData.get('email')
await db.users.create({ name, email })
}
重要なのは、これらの処理がサーバー側で実行されるという点です。クライアント側のJavaScriptバンドルサイズに影響を与えず、APIキーなどの機密情報も安全に扱えます。
ReactとNext.jsの役割分担
Server Actionについて調べると、「Reactの機能」「Next.jsの機能」という異なる説明を目にすることがあります。実際には、Reactが仕様を定義し、Next.jsがそれを実装するという関係です。
Reactの役割:
- Server Actionの基本仕様を定義
<form action={serverAction}>のようなインターフェースを提供- React 19で正式実装
Next.jsの役割:
- Reactの仕様を実装し、実行環境を提供
'use server'ディレクティブのビルド時処理- サーバーとクライアント間の通信層の実装
現時点では、Server Actionを実際に使えるのは主にNext.js(13.4以降)の環境となります。
従来のAPI Routesとの違い
Server Actionと従来のAPI Routesは、どちらもサーバー側で処理を実行しますが、アプローチが大きく異なります。
従来のAPI Routes:
[クライアント] → fetch() → [API Route] → [データベース]
↓ ↓
状態管理 ← JSONレスポンス ← 処理結果
Server Action:
[クライアント] → 直接呼び出し → [Server Action] → [データベース]
↓ ↓
自動的に反映 ← ← ← ← ← ← ← ← 処理完了
| 項目 | API Routes | Server Action |
|---|---|---|
| エンドポイント | 必要(/api/xxx) | 不要 |
| 呼び出し方法 | fetch()を記述 | 関数を直接呼び出し |
| 型安全性 | 手動で型定義 | 自動的に型推論 |
| コード量 | 多い | 少ない |
| JavaScript無効時 | 動作しない | 動作可能 |
特に注目すべきは、Server ActionではJavaScriptが無効でも動作するという点です。これはフォームの標準的なHTMLの仕組みを活用しているためで、アクセシビリティの観点からも優れています。
Server Action登場の背景
従来の開発フローの課題
従来のNext.jsでフォーム送信機能を実装する際、以下のような複数のステップが必要でした。
1. API Routeファイルの作成(約20行)
2. クライアント側のfetch処理(約15行)
3. 状態管理とエラーハンドリング(約15行)
シンプルなフォーム送信でも合計50行以上のコードと3つのファイルが必要でした。中小企業の開発現場では、この煩雑さが以下の問題を引き起こしていました。
- 開発時間の増加:単純な機能でも実装に時間がかかる
- 保守性の低下:コードが複数ファイルに分散し、全体像が把握しづらい
- バグの温床:fetch処理、エラーハンドリング、状態管理それぞれでミスが発生しやすい
データフローの複雑さ
従来の開発では、1つの機能を理解するために最低3つのファイルを確認する必要がありました。
UserForm.jsx (クライアント)
↓ handleSubmit関数
↓ fetch('/api/users')
↓
api/users.js (サーバー)
↓ バリデーション
↓ データベース処理
↓
UserForm.jsx (クライアント)
↓ 状態更新
↓ UI再レンダリング
さらに、以下のような問題も発生しがちでした。
型の不一致: クライアント側で送信するデータ形式とサーバー側で期待するデータ形式が一致しているか手動で確認が必要
エラーハンドリングの重複: クライアント側のバリデーション、サーバー側のバリデーション、ネットワークエラーの処理をそれぞれ別々に実装
状態管理の煩雑さ: loading、error、dataなどの状態を適切なタイミングで更新する必要がある
開発者体験の向上を目指して
Server Actionの登場背景には、開発者体験(DX)を向上させるというReactチームとNext.jsチームの強い意志があります。
特に中小企業の開発現場では、限られた人員とリソースで効率的に開発を進める必要があります。Server Actionは、煩雑な「配管工事」のようなコードを削減し、本質的な機能開発に集中できる環境を提供します。
Server Actionの主なメリット
コード量の大幅な削減
実際のコード比較で、その違いを確認してみましょう。
従来のAPI Route方式(約50行):
// pages/api/users.js
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
try {
const { name, email } = req.body
await db.users.create({ name, email })
res.status(200).json({ success: true })
} catch (error) {
res.status(500).json({ error: 'Failed to create user' })
}
}
// components/UserForm.jsx
export default function UserForm() {
const [loading, setLoading] = useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email }),
})
if (!response.ok) throw new Error('Failed')
} finally {
setLoading(false)
}
}
return <form onSubmit={handleSubmit}>{/* フォーム内容 */}</form>
}
Server Action方式(約15行):
// app/actions.js
'use server'
export async function createUser(formData) {
const name = formData.get('name')
const email = formData.get('email')
await db.users.create({ name, email })
}
// app/components/UserForm.jsx
import { createUser } from '../actions'
export default function UserForm() {
return (
<form action={createUser}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">送信</button>
</form>
)
}
削減率:約70%削減
この削減により、可読性の向上、保守性の向上、開発速度の向上、バグの削減という効果が得られます。
通信の効率化
Server Actionは、1回のネットワークラウンドトリップで処理が完結します。
従来の方法:
- ページ読み込み
- フォーム送信(POST /api/users)
- レスポンス受信
- 画面更新のためのデータ取得(GET /api/users)
Server Action:
- ページ読み込み
- フォーム送信 + 自動再検証(1回で完結)
この違いにより、ネットワークリクエスト数の削減、データ転送量の削減、レスポンス時間の短縮が実現します。特にモバイル環境や通信が不安定な環境では、この効率性の違いが顕著に表れます。
型安全性の向上
TypeScriptを使用している場合、Server Actionは型安全性の面でも大きなメリットがあります。
// actions.ts
'use server'
export async function createUser(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
// 戻り値の型も自動的に推論される
return { success: true, userId: 123 }
}
// components/UserForm.tsx
import { createUser } from '../actions'
// 関数の型が自動的に推論される
<form action={createUser}>
従来の方法では手動で型定義とアサーションが必要でしたが、Server Actionでは関数の型が自動的に推論され、コンパイル時のエラー検出やリファクタリングの安全性が向上します。
プログレッシブエンハンスメント対応
Server Actionの特に優れた特徴が、JavaScriptが無効な環境でも基本機能が動作するという点です。
'use server'
export async function createUser(formData) {
const name = formData.get('name')
await db.users.create({ name })
redirect('/users/success')
}
JavaScript有効時: フォーム送信がAjaxで実行され、ページ遷移なしで処理が完了
JavaScript無効時: 通常のフォーム送信として動作し、サーバー側で処理が実行される
この対応により、アクセシビリティの向上、SEOの改善、パフォーマンス向上、信頼性の確保が実現します。
Server Actionの注意点
学習コストと概念の理解
Server Actionは従来のWeb開発の常識とは異なる部分があり、最初は戸惑う可能性があります。
注意すべきポイント:
- 実行場所の理解
'use server'
export async function serverAction() {
console.log('これはサーバーのログに出力される')
}
- シリアライズ可能なデータのみ扱える
// OK: プリミティブ型、配列、オブジェクト
return { name: 'John', age: 30 }
// NG: 関数、クラスインスタンス
// return { callback: () => {} } // エラー
学習コストを下げるアプローチ:
- 小さく始める:まずは単純なフォーム送信から実装
- 公式ドキュメントを活用:Next.jsの公式ドキュメントは日本語対応
- 既存コードとの併用:段階的に移行することも可能
適切な使い分けが必要
Server Actionはすべてのケースに適しているわけではありません。
Server Actionが適している:
- フォーム送信(POST、PUT、DELETE)
- データベースへの書き込み操作
- 外部APIの呼び出し(機密情報を含む)
- ファイルアップロード
API Routesが適している:
- 外部サービスからのWebhook受信
- 複雑なRESTful API設計
- GET リクエストでのデータ取得
- 認証トークンの発行
セキュリティの考慮事項
Server Actionはサーバー側で実行されるため、適切なセキュリティ対策が必要です。
'use server'
export async function deleteUser(formData) {
// 必ず認証・認可チェックを実装
const session = await getSession()
if (!session || !session.user.isAdmin) {
throw new Error('権限がありません')
}
// 入力値のバリデーション
const userId = formData.get('userId')
if (!userId || typeof userId !== 'string') {
throw new Error('不正な入力です')
}
await db.users.delete({ where: { id: userId } })
}
セキュリティチェックリスト:
- 認証・認可の確認を必ず実装
- 入力値のバリデーションを徹底
- SQLインジェクション対策(ORMの使用)
- レート制限の実装
発展途上の機能
Server Actionは比較的新しい機能であり、以下の点に注意が必要です。
- ベストプラクティスがまだ確立途上
- エコシステムのライブラリが限定的
- エラーメッセージが分かりにくい場合がある
ただし、Next.jsの公式機能として積極的に開発が進められており、今後さらに改善されることが期待されます。
基本的な実装方法
「use server」ディレクティブの2つの書き方
1. ファイル全体をServer Actionにする(推奨)
// app/actions/users.js
'use server'
export async function createUser(formData) {
const name = formData.get('name')
await db.users.create({ name })
}
export async function updateUser(formData) {
const id = formData.get('id')
await db.users.update({ where: { id }, data: { name } })
}
2. 個別の関数をServer Actionにする
// app/components/UserForm.jsx
export default function UserForm() {
async function createUser(formData) {
'use server'
const name = formData.get('name')
await db.users.create({ name })
}
return <form action={createUser}>...</form>
}
実務での推奨パターン:
app/
├── actions/
│ ├── users.js # ユーザー関連
│ ├── orders.js # 注文関連
│ └── contacts.js # お問い合わせ関連
機能ごとにactionsディレクトリで分類することで、メンテナンスしやすいプロジェクトになります。
フォーム送信の実装
// app/actions/contacts.js
'use server'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
export async function submitContact(formData) {
const name = formData.get('name')
const email = formData.get('email')
const message = formData.get('message')
// バリデーション
if (!name || !email || !message) {
return { error: '必須項目を入力してください' }
}
// データベースに保存
await db.contacts.create({ name, email, message })
// キャッシュを更新
revalidatePath('/contacts')
return { success: true }
}
// app/components/ContactForm.jsx
import { submitContact } from '@/app/actions/contacts'
export default function ContactForm() {
return (
<form action={submitContact}>
<input type="text" name="name" required />
<input type="email" name="email" required />
<textarea name="message" required />
<button type="submit">送信する</button>
</form>
)
}
Server ComponentとClient Componentでの使い分け
Server Componentでの使用(シンプル):
// 'use client'の記述がない = Server Component
import { createUser } from '@/app/actions/users'
export default function UsersPage() {
return (
<form action={createUser}>
<input name="name" />
<button type="submit">登録</button>
</form>
)
}
Client Componentでの使用(リッチなUI):
'use client'
import { createUser } from '@/app/actions/users'
import { useFormState, useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? '送信中...' : '登録する'}
</button>
)
}
export default function UserForm() {
const [state, formAction] = useFormState(createUser, null)
return (
<form action={formAction}>
<input name="name" />
{state?.error && <p className="error">{state.error}</p>}
<SubmitButton />
</form>
)
}
使い分けの判断基準:
| ケース | 推奨 |
|---|---|
| シンプルなフォーム | Server Component |
| リアルタイムフィードバックが必要 | Client Component |
| 複雑なバリデーション | Client Component |
| 管理画面の一覧ページ | Server Component |
revalidatePathとredirectの活用
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createOrder(formData) {
const customerId = formData.get('customerId')
const items = JSON.parse(formData.get('items'))
// 注文を作成
const order = await db.orders.create({ data: { customerId, items } })
// 在庫を更新
for (const item of items) {
await db.products.update({
where: { id: item.productId },
data: { stock: { decrement: item.quantity } }
})
}
// 関連ページのキャッシュを更新
revalidatePath('/products')
revalidatePath('/orders')
// 注文完了ページへリダイレクト
redirect(`/orders/${order.id}/complete`)
}
実践的な活用例
お問い合わせフォーム
'use server'
import { db } from '@/lib/db'
import { sendEmail } from '@/lib/email'
export async function submitContactForm(formData) {
const name = formData.get('name')
const email = formData.get('email')
const message = formData.get('message')
// バリデーション
if (!name || !email || !message) {
return { error: '必須項目を入力してください' }
}
try {
// データベースに保存
await db.contacts.create({ data: { name, email, message } })
// 管理者へメール通知
await sendEmail({
to: process.env.ADMIN_EMAIL,
subject: `【新規お問い合わせ】`,
html: `<p>お名前: ${name}</p><p>内容: ${message}</p>`
})
// お客様へ自動返信
await sendEmail({
to: email,
subject: 'お問い合わせを受け付けました',
html: `<p>${name} 様</p><p>お問い合わせいただき、ありがとうございます。</p>`
})
return { success: true }
} catch (error) {
return { error: '送信中にエラーが発生しました' }
}
}
データベースのCRUD操作
'use server'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
// 新規登録
export async function createCustomer(formData) {
const name = formData.get('name')
const email = formData.get('email')
const customer = await db.customers.create({ data: { name, email } })
revalidatePath('/customers')
redirect(`/customers/${customer.id}`)
}
// 更新
export async function updateCustomer(formData) {
const id = formData.get('id')
const name = formData.get('name')
await db.customers.update({ where: { id }, data: { name } })
revalidatePath('/customers')
revalidatePath(`/customers/${id}`)
return { success: true }
}
// 削除(論理削除)
export async function deleteCustomer(formData) {
const id = formData.get('id')
await db.customers.update({ where: { id }, data: { deletedAt: new Date() } })
revalidatePath('/customers')
redirect('/customers')
}
ファイルアップロード
'use server'
import { writeFile } from 'fs/promises'
import { join } from 'path'
export async function uploadFile(formData) {
const file = formData.get('file')
// バリデーション
if (!file || file.size === 0) {
return { error: 'ファイルを選択してください' }
}
if (file.size > 5 * 1024 * 1024) {
return { error: 'ファイルサイズは5MB以下にしてください' }
}
if (file.type !== 'application/pdf') {
return { error: 'PDFファイルのみアップロード可能です' }
}
try {
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
const filename = `${Date.now()}_${file.name}`
const filepath = join(process.cwd(), 'public/uploads', filename)
await writeFile(filepath, buffer)
return { success: true, filepath: `/uploads/${filename}` }
} catch (error) {
return { error: 'アップロード中にエラーが発生しました' }
}
}
よくある疑問
API Routesとどちらを使うべきか
Server Actionを選ぶべきケース:
- フォーム送信などのPOST操作
- データベースへの書き込み
- ファイルアップロード
- 機密情報を扱う処理
API Routesを選ぶべきケース:
- 外部サービスからのWebhook
- GETリクエストでのデータ取得
- 複雑なRESTful API設計
- 認証トークンの発行
基本的には、フォーム関連の処理はServer Action、それ以外はAPI Routesという使い分けが推奨されます。
既存プロジェクトに導入すべきか
段階的な導入が可能です。
導入を推奨するケース:
- Next.js 13.4以降を使用している
- 新規機能の追加がある
- フォーム処理のリファクタリングを検討している
様子見を推奨するケース:
- 安定性を最優先する本番環境
- チームの学習時間が確保できない
- 既存のAPI Routesで問題が発生していない
まずは新規機能から試験的に導入し、効果を確認してから段階的に移行するのが安全です。
学習リソース
公式ドキュメント:
- Next.js公式ドキュメント(日本語対応)
- React公式ドキュメント
コミュニティ:
- Next.js GitHub Discussions
- Zenn、Qiitaの技術記事
- X(Twitter)の技術コミュニティ
実践的な学習方法:
- 公式ドキュメントのチュートリアルを実施
- 小さなサンプルプロジェクトで試す
- 既存プロジェクトの一部で試験導入
まとめ
Server Actionの本質
Server Actionは、従来の開発フローを簡素化し、開発者体験を向上させるための機能です。
主なポイント:
- コード量を約70%削減できる
- API Routeの作成が不要
- 型安全性が自動的に確保される
- JavaScriptが無効でも動作する
適切な技術選定のために
Server Actionは万能ではありません。プロジェクトの要件、チームのスキル、保守性などを総合的に判断して導入を検討してください。
判断基準:
- フォーム処理が多い → Server Action向き
- 複雑なAPI設計が必要 → API Routes向き
- 段階的な移行が可能 → 併用も選択肢
中小企業の開発現場では、限られたリソースで効率的に開発を進めることが重要です。Server Actionは、その課題に対する有力な選択肢の1つとなるでしょう。
まずは小さく始めて、自社のプロジェクトに合った活用方法を見つけていくことをおすすめします。
