Next.js 15 完全ガイド — App Router・Server Actions・最新機能まとめ
Next.js 15へのアップグレードを3つのプロダクションプロジェクトで経験しましたが、正直に言うと最初は地獄でした。cookies()が突然非同期になり、fetch cacheのデフォルトが変わり、既存コードが予想外の場所で壊れます。
ただし、一度乗り越えると戻れなくなります。Server ActionsでAPI routeを書く量が半分以下になり、Turbopackで開発サーバーの起動が体感3倍速くなりました。
この記事では、Next.js 15の主要機能を実際に踏んだ地雷も含めて解説します。公式ドキュメントだけでは分からない実務の知見を詰め込みました。
Next.js 15の主要アップデート
主な変更点
- React 19 サポート - React Compilerと完全統合
- Turbopack 安定版 - 開発サーバーが最大76%高速化
- Server Actions 安定版 - フォーム処理が劇的にシンプルに
- Partial Prerendering (PPR) 実験的サポート - 静的と動的の良いとこ取り
- async Request APIs - cookies(), headers()が非同期に
- fetch Cache のデフォルト変更 -
cache: 'no-store'がデフォルト
App Router完全ガイド
Next.js 15ではApp Routerが標準となりました。Pages Routerも使えますが、新規プロジェクトではApp Routerを使いましょう。
ディレクトリ構造
app/
├── layout.tsx # ルートレイアウト
├── page.tsx # トップページ(/)
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ ├── [slug]/
│ │ └── page.tsx # /blog/my-post
│ └── layout.tsx # /blog配下共通レイアウト
├── api/
│ └── users/
│ └── route.ts # API Route
└── error.tsx # エラーページ
page.tsx - ページコンポーネント
// app/blog/page.tsx
export default function BlogPage() {
return (
<div>
<h1>ブログ一覧</h1>
</div>
);
}
// ファイル名がURLになる
// page.tsx → アクセス可能
// BlogList.tsx → アクセス不可(コンポーネント)
layout.tsx - 共通レイアウト
// app/layout.tsx (ルートレイアウト)
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'My App',
description: 'Created with Next.js 15',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body>
<header>
<nav>Header Navigation</nav>
</header>
<main>{children}</main>
<footer>Footer Content</footer>
</body>
</html>
);
}
レイアウトはネスト可能です。
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="blog-container">
<aside>サイドバー</aside>
<div className="content">{children}</div>
</div>
);
}
ダイナミックルート
// app/blog/[slug]/page.tsx
type Props = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export default async function BlogPost({ params, searchParams }: Props) {
// Next.js 15ではparamsとsearchParamsが非同期に
const { slug } = await params;
const search = await searchParams;
return <h1>記事: {slug}</h1>;
}
// /blog/nextjs-guide → slug = 'nextjs-guide'
// /blog/react-tips?sort=date → slug = 'react-tips', search.sort = 'date'
Catch-all Routes と Optional Catch-all
// app/docs/[...slug]/page.tsx
// /docs/a → params.slug = ['a']
// /docs/a/b → params.slug = ['a', 'b']
// /docs/a/b/c → params.slug = ['a', 'b', 'c']
// app/shop/[[...slug]]/page.tsx (オプショナル)
// /shop → params.slug = undefined
// /shop/electronics → params.slug = ['electronics']
// /shop/electronics/phones → params.slug = ['electronics', 'phones']
Server ComponentsとClient Components
Next.js 15のApp Routerでは、デフォルトですべてのコンポーネントがServer Componentです。
Server Component(デフォルト)
// app/posts/page.tsx
// "use client" 指定なし = Server Component
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store', // Next.js 15のデフォルト
});
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
{posts.map((post: any) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
);
}
Server Componentのメリット:
- データフェッチがサーバー側で完結
- バンドルサイズが小さくなる(クライアントにJSを送らない)
- SEOに有利
- 環境変数やAPIキーを直接使える
Client Component
// app/components/Counter.tsx
'use client'; // この行が必須
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
Client Componentが必要な場合:
useState,useEffectなどのReact Hooksを使う- ブラウザAPI(
window,localStorage等)を使う - イベントハンドラ(
onClick,onChange等)を使う - カスタムフックを使う
組み合わせのベストプラクティス
// app/dashboard/page.tsx (Server Component)
import ClientCounter from '@/components/Counter'; // Client Component
async function getDashboardData() {
const res = await fetch('https://api.example.com/dashboard');
return res.json();
}
export default async function Dashboard() {
const data = await getDashboardData();
return (
<div>
<h1>Dashboard</h1>
<p>Total Users: {data.totalUsers}</p>
{/* Server ComponentからClient Componentを呼び出すのはOK */}
<ClientCounter />
</div>
);
}
重要: Server ComponentをClient Componentの子として渡すことはできません。
// ❌ これはNG
'use client';
import ServerComponent from './ServerComponent';
export default function ClientComponent() {
return <ServerComponent />; // エラー
}
// ✅ これはOK - childrenとして渡す
'use client';
export default function ClientWrapper({ children }: { children: React.ReactNode }) {
return <div className="wrapper">{children}</div>;
}
// 親(Server Component)から使う
<ClientWrapper>
<ServerComponent />
</ClientWrapper>
Server Actions - フォーム処理の革命
Server Actionsは、クライアントからサーバー側の関数を直接呼び出せる機能です。Next.js 15で正式に安定版となりました。
基本的な使い方
// app/actions/user.ts
'use server'; // Server Actionの宣言
import { revalidatePath } from 'next/cache';
export async function createUser(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
// データベースに保存
await db.user.create({
data: { name, email },
});
// キャッシュを再検証
revalidatePath('/users');
return { success: true };
}
フォームから呼び出す
// app/users/new/page.tsx
import { createUser } from '@/app/actions/user';
export default function NewUserPage() {
return (
<form action={createUser}>
<input name="name" type="text" placeholder="名前" required />
<input name="email" type="email" placeholder="メール" required />
<button type="submit">作成</button>
</form>
);
}
JavaScriptなしでも動作します。プログレッシブエンハンスメントの原則に沿った設計です。
useFormStateでローディング状態を管理
// app/users/new/page.tsx
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { createUser } from '@/app/actions/user';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '送信中...' : '作成'}
</button>
);
}
export default function NewUserPage() {
const [state, formAction] = useFormState(createUser, null);
return (
<form action={formAction}>
<input name="name" type="text" placeholder="名前" required />
<input name="email" type="email" placeholder="メール" required />
<SubmitButton />
{state?.error && <p className="error">{state.error}</p>}
</form>
);
}
Server Actionsのバリデーション
zodと組み合わせると型安全なバリデーションができます。
// app/actions/user.ts
'use server';
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().min(2, '名前は2文字以上必要です'),
email: z.string().email('有効なメールアドレスを入力してください'),
});
export async function createUser(formData: FormData) {
const rawData = {
name: formData.get('name'),
email: formData.get('email'),
};
// バリデーション
const result = UserSchema.safeParse(rawData);
if (!result.success) {
return {
error: result.error.flatten().fieldErrors,
};
}
// データベースに保存
await db.user.create({ data: result.data });
return { success: true };
}
データフェッチの新しいパターン
Next.js 15のfetchデフォルト変更
// Next.js 14以前: デフォルトでキャッシュされる
fetch('https://api.example.com/data'); // cache: 'force-cache'
// Next.js 15: デフォルトでキャッシュされない
fetch('https://api.example.com/data'); // cache: 'no-store'
明示的にキャッシュしたい場合:
// 60秒間キャッシュ
fetch('https://api.example.com/data', {
next: { revalidate: 60 }
});
// 永続的にキャッシュ
fetch('https://api.example.com/data', {
cache: 'force-cache'
});
Parallel Data Fetching
// 直列フェッチ(遅い)
const user = await fetchUser();
const posts = await fetchPosts(); // userの取得を待ってから実行
// 並列フェッチ(速い)
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts(),
]);
Sequential Data Fetching(意図的な直列)
async function Page() {
// userIdが必要なので、直列で取得
const user = await fetchUser();
const posts = await fetchPostsByUserId(user.id);
return <div>...</div>;
}
Async Request APIs
Next.js 15では、cookies(), headers(), params, searchParamsが非同期APIになりました。
// app/api/route.ts
import { cookies, headers } from 'next/headers';
export async function GET() {
// Next.js 15: awaitが必要
const cookieStore = await cookies();
const token = cookieStore.get('token');
const headersList = await headers();
const userAgent = headersList.get('user-agent');
return Response.json({ token, userAgent });
}
Partial Prerendering (PPR) - 実験的機能
PPRは静的部分と動的部分を同じページで組み合わせられる画期的な機能です。
// next.config.js
export default {
experimental: {
ppr: true,
},
};
// app/product/[id]/page.tsx
import { Suspense } from 'react';
// 静的部分
function ProductLayout() {
return (
<div>
<header>Static Header</header>
<Suspense fallback={<div>Loading...</div>}>
{/* 動的部分 */}
<DynamicContent />
</Suspense>
</div>
);
}
PPRにより、ページの静的部分は即座に表示され、動的部分はストリーミングで後から読み込まれます。
Turbopack - 開発サーバー高速化
# Next.js 15のデフォルト開発サーバー
npm run dev
# Turbopackを使用(next.config.jsで設定可能)
# 最大76%高速化
2026年現在、Turbopackはプロダクションビルドにも対応しています。
// next.config.js
export default {
experimental: {
turbo: {
// Turbopack設定
},
},
};
実務でハマるポイント — 公式ドキュメントに書いていないこと
Next.js 15を実プロジェクトで導入した際に遭遇しやすい問題をまとめます。
1. fetch cacheのデフォルト変更で本番データが古くならない
Next.js 14まではfetch()のデフォルトがcache: 'force-cache'でした。15からはcache: 'no-store'がデフォルトです。
起きること: アップグレード後、今まで静的生成されていたページが突然SSRになり、ビルド時間は短くなるがTTFBが遅くなります。
// Next.js 14 → 15で挙動が変わるコード
const res = await fetch('https://api.example.com/data')
// 14: キャッシュされる(高速だがデータが古い)
// 15: 毎回リクエスト(最新だが遅い)
// 明示的にキャッシュを指定する習慣をつけるべき
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // 1時間キャッシュ
})
2. cookies()/headers()のawait忘れでサイレントバグ
cookies()とheaders()が非同期APIになりましたが、TypeScriptの型チェックではawait忘れを検出できないケースがあります。
// ❌ awaitなし — エラーにならないが空オブジェクトを返す
const cookieStore = cookies()
const token = cookieStore.get('token') // undefined になる
// ✅ 正しい書き方
const cookieStore = await cookies()
const token = cookieStore.get('token')
3. Server ActionsでのバリデーションはZodを使え
Server Actionsはフォームデータをそのまま受け取るため、バリデーションなしだとXSSや不正データの温床になります。
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
})
export async function createUser(formData: FormData) {
const result = schema.safeParse({
email: formData.get('email'),
name: formData.get('name'),
})
if (!result.success) {
return { error: result.error.flatten() }
}
// 安全にDBに保存
}
4. ‘use client’の境界線の引き方
最も多い設計ミスは、大きなコンポーネントに'use client'をつけてしまうこと。クライアントバンドルが膨張します。
原則: 'use client'はできるだけ末端(リーフ)のコンポーネントにつける。
// ❌ ページ全体をclient componentに
app/dashboard/page.tsx → 'use client' → 全部がクライアントバンドルに
// ✅ インタラクティブな部分だけ分離
app/dashboard/page.tsx → Server Component(データ取得)
└── DashboardChart.tsx → 'use client'(グラフ描画のみ)
└── DashboardFilters.tsx → 'use client'(フィルターUIのみ)
Next.js 15が向いているケース・向いていないケース
| 向いている | 向いていない |
|---|---|
| 企業サイト・LP(SEOが重要) | リアルタイムチャットアプリ |
| ECサイト(商品ページの静的生成) | ゲーム・高FPSアプリ |
| ダッシュボード・管理画面 | 既にPages Routerで安定稼働中のアプリ |
| ブログ・メディアサイト | Reactに依存しないプロジェクト |
まとめ
Next.js 15のアップグレードは一筋縄ではいきませんが、乗り越えた先の開発体験は確実に向上します。
- fetch cacheのデフォルト変更を最初に確認する(本番でパフォーマンスが変わる)
- cookies()/headers()のawaitを検索置換でチェック
- Server ActionsにはZodバリデーションをセットで導入
- **‘use client’**はできるだけ末端コンポーネントに限定
- Turbopackは開発体験を劇的に改善する
Pages Routerからの移行は段階的に進めるのが現実的です。新規ページからApp Routerで書き始め、既存ページは動いているなら無理に移行しない — これが最もリスクの低いアプローチです。