Next.js の情報を調べていると、同じ「ページの作り方」でも pages/about.tsx と書かれている記事と app/about/page.tsx と書かれている記事の2種類が出てきます。これは Next.js に Pages Router と App Router という2つのルーティングの仕組みが並存しているためです。この記事では、ファイルの置き方・データ取得・レンダリング・フック・メタデータといった観点で両者が具体的にどう違うのかを対比しながら整理し、どちらを選べばよいかまで解説します。
目次
ルーターが2つある理由
Next.js はもともと、pages ディレクトリにファイルを置くとその名前が URL になる、という仕組みだけを持っていました。これが Pages Router です。長く使われてきた方式で、解説記事も既存のプロジェクトも大量にあります。
その後、React の Server Components を土台にした新しい仕組みとして app ディレクトリが Next.js 13 で追加され、13.4 で安定版になりました。これが App Router です。現在 create-next-app でプロジェクトを作ると既定で App Router の構成になります。
重要なのは、App Router が出たからといって Pages Router が廃止されたわけではないという点です。Pages Router は現在も引き続きサポートされており、両方を同じプロジェクトに置くこともできます。つまりこの2つは「新旧の置き換え」というより「並存している2つの選択肢」だと考えると理解しやすくなります。
ファイルの置き方が根本的に違う
一番目に見える違いが、URL とファイルの対応のさせ方です。Pages Router はファイル名がそのまま URL になります。pages/about.tsx を作れば /about でアクセスできます。
pages/
├── _app.tsx # 全ページ共通のラッパー
├── _document.tsx # html / body タグの調整
├── index.tsx # /
├── about.tsx # /about
├── 404.tsx # 見つからないときのページ
├── blog/
│ └── [id].tsx # /blog/hello など
└── api/
└── posts.ts # /api/posts
対して App Router はフォルダが URL、ファイル名は役割という考え方です。/about を作りたければ app/about/ というフォルダを作り、その中に page.tsx という決まった名前のファイルを置きます。page.tsx があるフォルダだけが URL として公開されるので、フォルダを作っただけではページになりません。
app/
├── layout.tsx # ルートレイアウト(html / body を含む)
├── page.tsx # /
├── not-found.tsx # 見つからないときのページ
├── about/
│ └── page.tsx # /about
├── blog/
│ └── [id]/
│ └── page.tsx # /blog/hello など
└── api/
└── posts/
└── route.ts # /api/posts
ファイル1つ分で済んでいたものがフォルダ+ファイルになるので、最初は冗長に感じるかもしれません。ただしこの形にしたことで、ページと同じフォルダにレイアウトやローディング表示、エラー画面といったそのルート専用のファイルを一緒に置けるようになっています。主な規約の対応は次のとおりです。
| 目的 | Pages Router | App Router |
|---|---|---|
| トップページ | pages/index.tsx | app/page.tsx |
| /about のページ | pages/about.tsx | app/about/page.tsx |
| 動的ルート(/blog/hello) | pages/blog/[id].tsx | app/blog/[id]/page.tsx |
| 全ページ共通のラッパー | pages/_app.tsx | app/layout.tsx |
| html / body タグの調整 | pages/_document.tsx | app/layout.tsx |
| 404 ページ | pages/404.tsx | app/not-found.tsx |
| エラー画面 | pages/500.tsx | app/error.tsx |
| 読み込み中の表示 | 規約なし(自分で実装) | app/loading.tsx |
| API | pages/api/posts.ts | app/api/posts/route.ts |
なお public ディレクトリの静的ファイルや next.config.ts の設定は、どちらのルーターでも共通です。next/link によるページ遷移の書き方も基本的に同じです。
API の書き方は別物になっている
ファイル規約の中でも、書き方が最も変わったのが API です。Pages Router の API Routes は、リクエストとレスポンスのオブジェクトを受け取る関数を default export します。Express などのミドルウェアに近い書き味で、メソッドの分岐は req.method を自分で見て行います。
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
// メソッドごとの処理は自分で分岐する
if (req.method !== 'GET') {
return res.status(405).json({ message: 'Method Not Allowed' });
}
res.status(200).json({ posts: [] });
}
App Router の Route Handlers は、route.ts の中で HTTP メソッド名の関数を名前付きで export します。受け取るのは Web 標準の Request で、返すのも Response です。Next.js 独自の型ではなくブラウザや他のランタイムでもおなじみの API を使う形になっています。
// GET /api/posts
export async function GET() {
return Response.json({ posts: [] });
}
// POST /api/posts
export async function POST(request: Request) {
const body = await request.json();
return Response.json({ received: body }, { status: 201 });
}
メソッドごとに関数が分かれるため、定義していないメソッドでアクセスされた場合は Next.js 側が 405 を返してくれます。分岐を自分で書かなくてよい分、見通しは良くなります。
データ取得は「専用の関数」から「コンポーネントの中」へ
2つのルーターで考え方が最も大きく変わるのがデータ取得です。Pages Router では、ページファイルから決められた名前の関数を export し、その戻り値がコンポーネントの props として渡されます。リクエストごとに実行したいなら getServerSideProps、ビルド時に一度だけ実行したいなら getStaticProps、動的ルートのパスを列挙するなら getStaticPaths という具合に、目的ごとに使う関数が決まっています。
import type { GetServerSideProps } from 'next';
type Post = { id: string; title: string };
type Props = { posts: Post[] };
// リクエストのたびにサーバーで実行される
export const getServerSideProps: GetServerSideProps<Props> = async () => {
const res = await fetch('https://api.example.com/posts');
const posts: Post[] = await res.json();
// 戻り値の props がコンポーネントに渡る
return { props: { posts } };
};
export default function PostsPage({ posts }: Props) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
App Router にはこれらの関数がありません。代わりに、ページコンポーネント自体を async 関数にして、その中で直接 await します。データを props としてバケツリレーする必要がないので、コードの見た目はかなりすっきりします。
type Post = { id: string; title: string };
// コンポーネント自体が async。サーバーで実行される
export default async function PostsPage() {
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store', // 毎回取得する(getServerSideProps 相当)
});
const posts: Post[] = await res.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
「毎回取得するのか、ビルド時に固めるのか」の切り替えは、関数名ではなく fetch のオプションやページ単位の設定で指定します。同じページの中で「この API はキャッシュする、こっちは毎回取る」と使い分けられるのが Pages Router との大きな違いです。
| やりたいこと | Pages Router | App Router |
|---|---|---|
| リクエストごとにサーバーで取得 | getServerSideProps | fetch(url, { cache: 'no-store' }) |
| ビルド時に取得して静的化 | getStaticProps | fetch(url, { cache: 'force-cache' }) |
| 一定時間ごとに再生成 | getStaticProps の revalidate | fetch の next.revalidate や export const revalidate |
| 動的ルートのパスを列挙 | getStaticPaths | generateStaticParams |
| ブラウザ側で取得 | useEffect や SWR など | 同じ(ただし 'use client' が必要) |
キャッシュの既定値はバージョンによって変わってきた部分なので、明示的に指定しておくと迷いません。Next.js 15 以降では fetch の結果は既定でキャッシュされないため、静的に固めたい箇所には cache: 'force-cache' を書くのが確実です。
もう1つ注意したいのが、getServerSideProps や getStaticProps は app ディレクトリの中では一切動かないことです。書いてもただの export として無視され、データが取れずに undefined になります。Pages Router 向けの記事のコードをそのまま app に貼ると、この形ではまることが多いので気をつけてください。
既定がクライアントかサーバーかという前提の違い
Pages Router のコンポーネントは、すべて従来どおりの React コンポーネントです。サーバーで HTML に描き出されたあと、同じコードがブラウザにも送られて動きます。そのため useState や onClick をどのファイルに書いても構いません。
App Router では、app ディレクトリのコンポーネントは既定で Server Components になります。これはサーバー側でだけ実行され、ブラウザには実行結果だけが送られるコンポーネントです。データベースに直接アクセスしたり API キーを使ったりしても、そのコードがブラウザに届かないという利点があります。半面、useState や useEffect、onClick のようなブラウザで動く必要があるものは使えません。
そこで登場するのが 'use client' です。ファイルの先頭にこの1行を書くと、そのファイル以降は従来どおりのクライアントコンポーネントとして扱われ、状態やイベントハンドラーが使えるようになります。
'use client'; // これがないと useState が使えない
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count} 回
</button>
);
}
'use client' は「このコンポーネントだけをクライアント側にする」というより、サーバーとクライアントの境界を宣言するものです。この指定をしたファイルから import されたコンポーネントも、まとめてクライアント側で扱われます。つまり 'use client' を app/layout.tsx のような上位のファイルに書いてしまうと、ほぼ全部がクライアントコンポーネントになり、Server Components の利点が消えてしまいます。状態を使う末端のコンポーネントに絞って付けるのが基本です。
この前提の違いは、外部ライブラリを使うときにも影響します。内部で useState やブラウザ API を使っているライブラリは Server Components から直接呼べないため、'use client' を書いたラッパーコンポーネントを経由させる必要があります。Pages Router では意識しなくてよかった作業なので、移行時につまずきやすいポイントです。
ルーティング用のフックは import 元も中身も変わる
Pages Router では next/router の useRouter が窓口で、遷移も現在のパスもクエリ文字列もすべてこの1つのオブジェクトから取れます。動的ルートの値もクエリ文字列も、まとめて router.query に入ります。
import { useRouter } from 'next/router';
export default function BlogPost() {
const router = useRouter();
// 動的ルートの [id] も ?page=2 も query から取れる
const { id, page } = router.query;
return (
<div>
<p>現在のパス: {router.pathname}</p>
<p>記事 ID: {id}/ページ: {page}</p>
<button onClick={() => router.push('/')}>トップへ</button>
</div>
);
}
App Router では next/navigation から import します。同じ useRouter という名前ですが中身は別物で、push や refresh といった操作系だけを持ち、pathname や query は含まれていません。現在のパスは usePathname、クエリ文字列は useSearchParams、動的ルートの値は useParams と、目的ごとに別のフックへ分かれています。
'use client'; // これらのフックはクライアントコンポーネント専用
import { useRouter, usePathname, useSearchParams, useParams } from 'next/navigation';
export default function Toolbar() {
const router = useRouter();
const pathname = usePathname(); // '/blog/hello'
const searchParams = useSearchParams(); // ?page=2 を読む
const params = useParams(); // { id: 'hello' }
const page = searchParams.get('page');
return (
<div>
<p>現在のパス: {pathname}</p>
<p>記事 ID: {params.id}/ページ: {page}</p>
<button onClick={() => router.push('/')}>トップへ</button>
</div>
);
}
| 用途 | Pages Router(next/router) | App Router(next/navigation) |
|---|---|---|
| 画面遷移 | useRouter().push() | useRouter().push() |
| 現在のパス | router.pathname / router.asPath | usePathname() |
| クエリ文字列 | router.query | useSearchParams() |
| 動的ルートの値 | router.query | useParams() またはページの params |
| 再取得・再描画 | 該当なし | router.refresh() |
フックを使わずにサーバー側で値を受け取ることもできます。App Router の page.tsx は params と searchParams を props として受け取れるので、単に URL の値を表示したいだけならクライアントコンポーネントにする必要はありません。なお useSearchParams を静的に生成されるページで使う場合は、その部分を Suspense で囲む必要がある点にも注意してください。
メタデータは JSX からオブジェクトの export へ
ページのタイトルや description の指定方法も変わります。Pages Router では next/head の Head コンポーネントを JSX の中に書き、その子要素として title や meta タグを並べます。
import Head from 'next/head';
export default function AboutPage() {
return (
<>
<Head>
<title>会社概要 | Example</title>
<meta name="description" content="Example の会社概要です。" />
</Head>
<h1>会社概要</h1>
</>
);
}
App Router では metadata という名前のオブジェクトを export します。JSX ではなくデータとして書くため型が付き、タイトルの区切り文字や Open Graph の設定を親レイアウトから継承させることもできます。
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '会社概要 | Example',
description: 'Example の会社概要です。',
};
export default function AboutPage() {
return <h1>会社概要</h1>;
}
記事タイトルのように内容によって変わる場合は、generateMetadata という関数を export します。params を受け取れる非同期関数なので、その中でデータを取得してタイトルに使えます。
import type { Metadata } from 'next';
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>;
}): Promise<Metadata> {
const { id } = await params;
const res = await fetch(`https://api.example.com/posts/${id}`);
const post: { title: string } = await res.json();
return { title: post.title };
}
ここで見落としやすいのが、metadata と generateMetadata は Server Components でしか使えないことです。'use client' を書いたファイルで export しても効きません。状態を持つページのメタデータを設定したい場合は、page.tsx はサーバー側のままにして、状態が必要な部分だけを別のクライアントコンポーネントに切り出す形にします。
共通レイアウトの扱いとネストの考え方
Pages Router で全ページ共通のヘッダーなどを置く場所は _app.tsx です。ここはアプリ全体で1つだけなので、「管理画面だけ別のサイドバーを出したい」といった部分的な共通レイアウトは、ページごとにラッパーを書くなどの工夫が必要でした。
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<header>共通ヘッダー</header>
<Component {...pageProps} />
</>
);
}
App Router の layout.tsx は、どのフォルダにも置けるのが特徴です。app/layout.tsx はアプリ全体、app/admin/layout.tsx は /admin 以下だけ、というように階層ごとの共通部分を自然に表現できます。レイアウトは入れ子になり、外側から順に children として包まれていきます。
// ルートレイアウトには html と body が必要(_document.tsx の役割も兼ねる)
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body>
<header>共通ヘッダー</header>
{children}
</body>
</html>
);
}
もう1つ実用上うれしいのが、ページを移動してもレイアウトは再レンダリングされず、状態が保持される点です。サイドバーの開閉状態やスクロール位置が遷移のたびにリセットされない、といった挙動が規約だけで実現できます。
pages と app は同じプロジェクトに共存できる
ここまで違いを並べると全面的な書き換えが必要に見えますが、実際には pages と app は同じプロジェクトに同時に置けます。既存のページはそのまま pages に残し、新しく作るページだけ app に置く、という進め方ができます。
ただし条件があります。両方に置いたルートが同じ URL に解決されてはいけません。たとえば pages/about.tsx と app/about/page.tsx はどちらも /about になるため、この状態ではビルド時にエラーになります。URL の重複を防ぐための仕様で、優先されるのは App Router 側です。1つの URL は必ずどちらか一方が担当する、と考えてください。
段階的に移行するなら、app ディレクトリを追加してルートレイアウトを用意し、新規ページから app で書き始めるのが現実的です。既存ページを移すときは URL 単位で1つずつ入れ替えます。_app.tsx の共通処理はルートレイアウトに、getServerSideProps は Server Component 内の fetch に、next/router は next/navigation に、というように前述の対応表を順に当てはめていく作業になります。
どちらを選ぶべきか
新しく作るなら App Router
これから始めるプロジェクトなら App Router を選ぶのが基本です。create-next-app の既定であり、公式ドキュメントも App Router を主軸に書かれています。ネストレイアウトやローディング表示の規約、Server Components によるデータ取得といった機能も App Router 側にあります。学習コストとしては Server Components の考え方に慣れる必要がありますが、これから調べる情報が App Router 前提で書かれている以上、そちらに合わせたほうが結果的に迷いにくくなります。
既存の Pages Router は無理に移行しなくてよい
すでに Pages Router で動いているアプリを、機能追加の予定もないのに書き換える必要はありません。Pages Router は引き続きサポートされており、動いているものを壊すリスクのほうが大きいためです。移行を検討するとしても一気にやる必要はなく、共存できる性質を使って新規ページから少しずつ移すのが安全です。
調べ物をするときは、どちらの記事か必ず確認する
実務で一番困るのは、検索で出てきたコードがどちらのルーター向けか分からないまま貼ってしまうことです。getServerSideProps が出てきたら Pages Router、'use client' や app/ のパスが出てきたら App Router、というように判別できます。公式ドキュメントも URL が /docs/app/... と /docs/pages/... で分かれているので、自分の構成に合ったほうを開いているか確認する習慣をつけると、原因不明のエラーをかなり減らせます。
まとめ
Pages Router はファイル名がそのまま URL になる従来の方式、App Router は Next.js 13 で追加され 13.4 で安定版になった、フォルダが URL・ファイル名が役割という方式です。pages/about.tsx と app/about/page.tsx、_app.tsx と layout.tsx、pages/404.tsx と not-found.tsx、pages/api/posts.ts と app/api/posts/route.ts といった具合に、規約が1つずつ対応しています。
中身の違いとしては、データ取得が getServerSideProps などの専用関数から Server Component 内の await fetch に変わったこと、App Router では既定が Server Components で状態を使うには 'use client' が要ること、フックが next/router から next/navigation に移って router.query が無くなったこと、メタデータが next/head から metadata / generateMetadata の export になったことが柱です。両者は同じプロジェクトに共存でき(同じ URL の重複は不可)、新規は App Router、既存の Pages Router は無理に移行しない、という判断でおおむね問題ありません。