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公式ドキュメント

K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →