更新: 2026年03月 · 10 分で読める · 5,006 文字
Next.js App Routerへの移行を5ステップで完了させる実践手順
本記事では、Pages Routerで構築されたNext.jsプロジェクトをApp Routerに移行する具体的な手順を解説します。段階的なアプローチにより、既存機能を保ちながら安全に移行を進められます。
Next.js App Router移行が必要な理由
Next.js 13から導入されたApp Routerは、Pages Routerから大きく進化しました。Server Componentsのネイティブサポート、レイアウトシステムの改善、より直感的なファイル構造が特徴です。Pages Routerは今後もサポートされますが、新しいプロジェクトやメンテナンス効率を考えるとApp Routerへの移行は重要な判断です。
移行前の準備確認
環境とバージョン確認
まず、現在の環境を確認します。本記事の動作確認環境は以下の通りです:
- macOS 14 / Ubuntu 22.04
- Next.js 13.5 以上
- Node.js 18.17 以上
- npm または yarn
Next.jsのバージョン確認コマンド:
npm list next
プロジェクト構造の把握
移行前に、現在のpages/ディレクトリ構造を全て把握しておきます。特に以下の点をチェックしてください:
- _app.jsと_document.jsの内容
- APIルートの一覧
- 動的ルートの使用パターン
- getServerSideProps・getStaticPropsの使用箇所
ステップ1: app/ディレクトリの作成と基本ファイルの準備
App Routerはpages/ディレクトリの代わりにapp/ディレクトリを使用します。Pages Routerと並行動作可能なため、段階的移行が可能です。
// 新しいapp/ディレクトリ構造
app/
├── layout.tsx // ルートレイアウト(_app.jsの後継)
├── page.tsx // ホームページ
├── globals.css
└── components/
ルートレイアウトの作成
// app/layout.tsx
import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: 'My App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ja">
<body>
{children}
</body>
</html>
)
}
重要ポイント:Pages Routerの_app.js内のグローバルスタイルインポートはRootLayout内で行う必要があります。
ステップ2: ホームページと基本ページの移行
Pages Routerとの構文の違い
以下は移行前後の比較です:
// Pages Router(pages/index.js)
export default function Home() {
return <div>ホームページ</div>
}
// App Router(app/page.tsx)
export default function Home() {
return <div>ホームページ</div>
}
基本的な構文は同じですが、ファイルネーミングと配置が異なります。
複数ページの移行例
// 移行前(Pages Router)
pages/
├── index.js
├── about.js
└── blog/
└── [slug].js
// 移行後(App Router)
app/
├── page.tsx
├── about/
│ └── page.tsx
└── blog/
└── [slug]/
└── page.tsx
ステップ3: APIルートとデータフェッチングの移行
APIルートの変更
// Pages Router(pages/api/users.js)
export default function handler(req, res) {
if (req.method === 'GET') {
res.status(200).json({ users: [] })
}
}
// App Router(app/api/users/route.ts)
export async function GET(request: Request) {
return Response.json({ users: [] })
}
export async function POST(request: Request) {
const data = await request.json()
return Response.json({ success: true })
}
Server ComponentでのデータフェッチングとgetStaticPropsの置き換え
// Pages Router(getStaticPropsを使用)
export async function getStaticProps() {
const res = await fetch('https://api.example.com/posts')
const posts = await res.json()
return {
props: { posts },
revalidate: 60
}
}
export default function Blog({ posts }) {
return (
<div>
{posts.map(post => <div key={post.id}>{post.title}</div>)}
</div>
)
}
// App Router(Server Componentでダイレクトにfetch)
async function fetchPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 } // ISR相当
})
return res.json()
}
export default async function Blog() {
const posts = await fetchPosts()
return (
<div>
{posts.map(post => <div key={post.id}>{post.title}</div>)}
</div>
)
}
App Routerでは、Server Componentがデフォルトのため、async/awaitでダイレクトにデータベースやAPIを呼び出せます。
ステップ4: Client Componentと状態管理の適切な配置
Client Componentの明示的指定
App Routerでは、インタラクティブな機能が必要なコンポーネントに「use client」ディレクティブを追加します:
// app/components/Counter.tsx
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
カウント: {count}
</button>
)
}
ハマりポイント: 混在するコンポーネント構造
よくある失敗は、Server ComponentからClient Componentに状態を渡そうとする場合です。以下の構造が正しい方法です:
// app/dashboard/page.tsx(Server Component)
import Counter from '@/app/components/Counter'
export default function Dashboard() {
// Server側でデータを取得
const data = { title: 'ダッシュボード' }
return (
<div>
<h1>{data.title}</h1>
<Counter /> {/* Client Componentはこのまま使用可 */}
</div>
)
}
ステップ5: 環境変数とMiddlewareの設定
環境変数の設定
.env.localファイルはPages Routerと同じ方法で動作します:
# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
API_SECRET_KEY=secret123
Middlewareの設定(オプション)
// middleware.ts(プロジェクトルート)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 認証チェックなどを実装
if (request.nextUrl.pathname.startsWith('/admin')) {
const token = request.cookies.get('auth-token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
return NextResponse.next()
}
export const config = {
matcher: ['/admin/:path*']
}
移行時の実践的チェックリスト
- 全ページが表示される: app/配下の全ページを確認
- APIが正常に動作: APIエンドポイントへのリクエストテスト
- スタイルが適用される: グローバルCSS、モジュールCSSの確認
- 環境変数が読み込まれる: console.logで確認
- ビルド成功: npm run buildでエラーがないこと
- 本番デプロイ前のテスト: npm run buildと npm run startで検証
よくある質問
はい、Next.js 13以降では両方を同時に使用できます。pages/とapp/ディレクトリが共存する場合、app/ルートが優先されます。段階的移行に最適です。ただし、本番環境では統一することを推奨します。
App Routerでは、Server Components内でのダイレクトなfetch呼び出しに統合されました。キャッシング戦略はfetch()の第二引数でコントロールします。例えば、next: { revalidate: 60 }はISRの60秒キャッシュに相当します。
最初にapp/配下に移行したファイルのみを配置し、pages/を一時的に削除してからビルドしてください。エラーメッセージから原因を特定し、pages/内の残りファイルを確認します。「use client」ディレクティブの不足やServer Componentでのクライアント機能の使用が一般的な原因です。
まとめ
- App Routerは段階的移行が可能で、Pages Routerと共存できます
- app/layout.tsxはPages Routerの_app.jsに相当する最上位のレイアウトです
- Server Componentsがデフォルトで、async/awaitでデータフェッチが簡潔になります
- インタラクティブ機能は「use client」で明示的にClient Componentとして指定します
- APIルートはroute.tsファイルで、HTTPメソッドごとに関数を分けて定義します
- 移行前に現在の構造を完全に把握し、小さな単位で段階的に進めることがコツです
参考資料:Next.js App Router公式ドキュメント
おすすめフロントエンドリソース
- MDN Web Docs The most trusted reference for HTML, CSS, and JavaScript.
- React Documentation Official React tutorials and API reference.
- Can I Use Essential tool for checking browser compatibility.