Next.js App Router の使い方 – Pages Router との違いと実装パターン

目次

Next.js App Router の使い方 – Pages Router との違いと実装パターン

Pages Router から App Router へ

Next.js 13 で大きな変更が入りました。それが「App Router」です。

従来の Pages Router は「pages/ ディレクトリ = ファイルパス = URL」という、シンプルでわかりやすい仕組みでした。しかし App Router は「app/ ディレクトリ」を導入し、ファイル構造はより複雑になった一方で、レイアウト管理やサーバーコンポーネントなど、強力な機能が追加されました。

今回は、App Router の基本から実装パターンまで、「Pages Router からの移行」という視点でまとめておきたいと思います。

もしご自身で先に公式ドキュメントをご覧になられる方は下のリンクから眺めてみてください。
Next.js App Router – Official Docs ー 公式ドキュメント(英語)

対象となる方

  • Next.js を使っている方
  • App Router への移行を検討している方
  • サーバーコンポーネントに興味のある方

 ※ このドキュメントは Next.js 14 以上で書いていきます。

ファイル構造の違い

Pages Router(従来)

pages/
├── index.tsx           # GET / → ホーム
├── about.tsx           # GET /about
├── blog/
│   ├── index.tsx       # GET /blog
│   └── [slug].tsx      # GET /blog/:slug(動的ルート)
└── api/
    └── users.ts        # POST /api/users(API ルート)

特徴: ファイルパス = URL、シンプルで予測可能

App Router(新)

app/
├── layout.tsx          # ルートレイアウト
├── page.tsx            # GET /
├── about/
│   └── page.tsx        # GET /about
├── blog/
│   ├── layout.tsx      # /blog 専用レイアウト
│   ├── page.tsx        # GET /blog
│   └── [slug]/
│       └── page.tsx    # GET /blog/:slug
└── api/
    └── users/
        └── route.ts    # POST /api/users(API ルート)

特徴: ディレクトリ = セグメント、layout.tsx でレイアウト階層管理

Server Components vs Client Components

App Router の最大の特徴は「Server Components がデフォルト」ということです。

Server Component(サーバーで実行)

// app/products/page.tsx
// デフォルトは Server Component

export default async function ProductsPage() {
  // サーバーで実行される
  const products = await fetch('https://api.example.com/products', {
    cache: 'force-cache',  // ISR(Incremental Static Regeneration)
  }).then(r => r.json())

  return (
    <div>
      <h1>Products</h1>
      <ul>
        {products.map(p => (
          <li key={p.id}>{p.name}</li>
        ))}
      </ul>
    </div>
  )
}

// 利点:
// - DB に直接クエリできる
// - API キーが漏洩しない
// - JavaScript バンドルが小さい
// - SEO に有利

Client Component(ブラウザで実行)

'use client'  // ← この行で Client Component に変換

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  )
}

// 用途:
// - useState、useEffect など状態が必要
// - クリック・フォーム入力などのインタラクション
// - ブラウザ API(localStorage など)を使う

実装例:ブログサイト

ディレクトリ構成

app/
├── layout.tsx              # ルートレイアウト(ナビゲーション)
├── page.tsx                # ホーム
└── blog/
    ├── layout.tsx          # ブログ用レイアウト(サイドバー)
    ├── page.tsx            # /blog(記事一覧)
    └── [slug]/
        └── page.tsx        # /blog/:slug(記事詳細)

ルートレイアウト(app/layout.tsx)

export const metadata = {
  title: 'My Blog',
  description: 'Welcome to my blog',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ja">
      <body>
        <nav>
          <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/blog">Blog</a></li>
          </ul>
        </nav>
        <main>{children}</main>
      </body>
    </html>
  )
}

ブログ一覧(app/blog/page.tsx)

export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 },  // 1 時間ごとに再生成
  }).then(r => r.json())

  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <a href={`/blog/${post.slug}`}>
              {post.title}
            </a>
          </li>
        ))}
      </ul>
    </div>
  )
}

記事詳細(app/blog/[slug]/page.tsx)

export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())
  return posts.map(post => ({
    slug: post.slug,
  }))
}

export default async function BlogPostPage({
  params,
}: {
  params: { slug: string }
}) {
  const post = await fetch(
    `https://api.example.com/posts/${params.slug}`,
    { next: { revalidate: 3600 } }
  ).then(r => r.json())

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.published_at}</p>
      <div>{post.content}</div>
    </article>
  )
}

API ルート(Server Actions)

従来の API ルート(pages/api)も、App Router では app/api/ に変わりました。

// app/api/posts/route.ts

import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  const posts = [
    { id: 1, title: 'Post 1', slug: 'post-1' },
    { id: 2, title: 'Post 2', slug: 'post-2' },
  ]
  return NextResponse.json(posts)
}

export async function POST(request: NextRequest) {
  const body = await request.json()
  const newPost = {
    id: 3,
    title: body.title,
    slug: body.slug,
  }
  return NextResponse.json(newPost, { status: 201 })

Pages Router との実装比較

機能Pages RouterApp Router
ファイル構造pages/ ディレクトリapp/ ディレクトリ
レイアウト管理_app.tsx で全体管理各セグメントで layout.tsx
データ取得(ISR)getStaticProps + revalidatenext: { revalidate } で fetch
サーバー処理getServerSidePropsServer Component がデフォルト
API ルートpages/api/ でシンプルapp/api/ で NextRequest/Response

Pages Router からの移行チェックリスト

  • pages/ を app/ に移行した
  • getStaticProps / getServerSideProps を削除して fetch に統一した
  • _app.tsx のロジックを root layout.tsx に移した
  • インタラクティブな部分に “use client” を追加した
  • useRouter をテストして動作確認した
  • ISR 設定(revalidate)を確認した

まとめ

以上で Next.js App Router について説明を終えたいと思います。

App Router は「複雑になった」という印象を持つ人も多いですが、実は「Server Components がデフォルト」という哲学的な変更を中心に、その周りが整理されたに過ぎません。

Server Components の登場で「サーバーとクライアントの役割分離」がより明確になり、結果として「小さく・高速で・SEO に強い」ウェブアプリが作りやすくなったんです。

Pages Router から App Router へ。新しい時代の Next.js を使いこなすことで、より良いウェブアプリケーション体験が実現できますね ^^v

ということで、ではまた〜

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

Web Developer / Educator