App Routerとは?Next.jsのファイル規約・動的ルーティング・Pages Routerとの違い
App Routerは、Next.jsのappディレクトリでフォルダとファイルの配置そのものをURLの定義にするルーティング方式です。2023年5月のNext.js 13.4で安定版になり、それ以降に作られたプロジェクトの標準になりました。一方で、13系が公開された当時の記述が残る解説も多く、2026年8月時点の最新安定版16.3.0でそのまま書き換えると動かない、あるいはエラーも出ないまま値が壊れる箇所があります。この記事ではファイル規約と動的ルーティングの書き方を押さえたうえで、16.3.0を実際にインストールしてビルドし、13時代の記述がどこで通用しなくなったかまで確認します。
まとめ
App Routerはapp配下のフォルダ構造がそのままURLになる仕組みで、pageがルートを作り、layoutが共有UIを担います。Pages RouterのgetServerSidePropsやnext/headに相当する仕組みは置き換えられており、移行時はこの3点(データ取得・メタデータ・useRouterの参照先)でまず詰まります。
実務でいちばん危険なのは、13時代のサンプルをそのまま持ち込んだときに本番ビルド経路では何の異常も出ないまま値だけが壊れるケースです。Next.js 16ではparamsがPromiseになったため、同期アクセスすると型注釈を書き換えただけでビルドが通り、描画結果がundefinedになります。next devなら明示的なエラーが出ますが、next buildとnext startの経路では出ません。手元の16.3.0で両方の挙動を確認しました。以下、ファイル規約から順に見ていきましょう。
App Routerの定義と13.4での安定版化
App Routerは、appディレクトリ内のフォルダをURLセグメントに対応させ、その中に置く決められた名前のファイルでUIを定義するルーティング方式です。ルートを設定ファイルに列挙する必要はありません。フォルダを掘ってpage.tsxを置けば、その時点でURLが増えます。
安定版になったのはNext.js 13.4です。公式ブログは2023年5月4日付で「Next.js 13.4 is a foundational release, marking stability for the App Router」と宣言し、React Server Components、ネストされたルートとレイアウト、ストリーミングとSuspenseを安定機能として挙げました。同じ記事のFAQでPages Routerの扱いも明言されており、「We are committed to supporting pages/ development, including bug fixes, improvements, and security patches, for multiple major versions moving forward」と継続サポートを約束しています。実際、最新安定版16.3.0のタグを見てもドキュメントには02-pagesのディレクトリが残っており、Pages Routerは廃止されていません。
App Routerのコンポーネントは既定でサーバー側で実行されます。この仕組みそのものについてはReact Server Components(RSC)とは?サーバー・クライアントの違いと使い分けを実装例で解説で扱っているため、本記事はルーティングとファイル規約に絞ります。
appディレクトリのファイル規約と実機ビルドで生成されたルート
ルートを定義するpageと共有UIを担うlayout
覚えるべき最小単位は2つです。pageを置いたフォルダだけが公開URLになり、layoutはそのフォルダ以下で共有されるUIになります。フォルダを作っただけではURLは生えません。
app/
layout.tsx # ルートレイアウト(必須・html と body を含む)
page.tsx # URL: /
not-found.tsx # 404時のUI
blog/
layout.tsx # /blog 配下で共有されるUI
loading.tsx # /blog 配下の読み込み中UI
page.tsx # URL: /blog
[slug]/
page.tsx # URL: /blog/任意の値
docs/
[[...slug]]/
page.tsx # URL: /docs と /docs/任意の階層
(marketing)/
pricing/
page.tsx # URL: /pricing(括弧付きフォルダはURLに出ない)
api/
health/
route.ts # URL: /api/health(APIエンドポイント)
ルートレイアウトは必須で、htmlタグとbodyタグを自分で書きます。Pages Routerの_app.jsと_document.jsが担っていた役割が、この1ファイルに統合されたと考えると対応が取りやすくなります。
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja">
<body>{children}</body>
</html>
)
}
ネストされたフォルダにlayoutを追加すると、親のレイアウトが子のレイアウトを包む形で入れ子になります。ページ遷移してもレイアウトは再レンダリングされず状態を保持する、と公式ドキュメントは説明しています。
loading・not-found・routeの役割分担
pageとlayout以外にも予約ファイル名があり、それぞれ配置した階層に対して効きます。よく使うものだけ挙げましょう。
| ファイル名 | 役割 | 効く範囲 |
|---|---|---|
| page | そのURLの本体 | 置いたフォルダ |
| layout | 共有UI・状態を保持 | 配下すべて |
| loading | 読み込み中UI | 配下すべて |
| not-found | 404時のUI | 配下すべて |
| error | 例外発生時のUI | 配下すべて |
| route | APIエンドポイント | 置いたフォルダ |
| default | パラレルルートの既定表示 | 置いたスロット |
同じフォルダにpageとrouteを同居させることはできません。手元で試すとConflicting route and page at /blogでビルドが止まります。UIを返すのがpage、レスポンスを返すのがrouteという住み分けです。なおdefaultはNext.js 16で扱いが変わり、パラレルルートのスロットすべてに明示的なdefaultが必要になりました。無いとビルドが失敗します。
実機ビルドで出力されたルート一覧
上のツリーどおりにファイルを置き、next buildを実行した結果が次の出力です。Next.js 16.3.0とReact 19.2.0、Node.js v26.5.0の環境で取得しました。
▲ Next.js 16.3.0 (Turbopack)
✓ Running next.config took 38ms
Creating an optimized production build ...
✓ Compiled successfully in 7.0s
(中略:TypeScript 検査・ページデータ収集・静的ページ生成・最適化)
Route (app)
┌ ○ /
├ ○ /_not-found
├ ƒ /api/health
├ ○ /blog
├ ƒ /blog/[slug]
├ ƒ /docs/[[...slug]]
└ ○ /pricing
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
読み取れることが2つあります。1つはバナーの「(Turbopack)」で、オプションを何も付けていないのにTurbopackが使われている点。もう1つは○とƒの区別で、/blogは静的に事前生成され、/blog/[slug]はリクエストごとにサーバーで描画されるという判定がビルド時点で確定します。どのルートがどちらに倒れるかは、ここを見れば一目で分かります。静的生成と動的描画の選び分けそのものはNext.jsのレンダリング方式|SSG・ISR・SSR・CSRの違いとApp Routerでの選び方で整理しました。
動的セグメントとルートグループの記法
[slug]と[[...slug]]の使い分け
フォルダ名を角括弧で囲むと動的セグメントになります。[slug]は1階層ぶんを受け取り、[...slug]は残りのパスをすべて配列で受け取り、[[...slug]]はセグメントが1つも無い場合にもマッチします。上のビルド結果で/docs/[[...slug]]が1本のルートとして登録されているのは、/docsと/docs/a/bの両方を同じファイルで受けているためです。
// app/blog/[slug]/page.tsx
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
return <h1>{slug}</h1>
}
ここで押さえるべきはparamsがPromiseである点です。非同期化そのものはNext.js 15の変更で、当時は同期アクセスも暫定的に許容されていました。16の移行ガイドは「Version 15 introduced Async Request APIs as a breaking change, with temporary synchronous compatibility. Starting with Next.js 16, synchronous access is fully removed」と述べており、16で猶予が終わっています。クライアントコンポーネントで受け取る場合はReactのuseで解決します。動的セグメントはpageだけでなくlayout、route、generateMetadataにも同じparamsとして渡されます。
括弧付きフォルダがURLから消える挙動の実測
フォルダ名を丸括弧で囲むとルートグループになり、そのフォルダはURLに現れません。管理画面とマーケティングページでレイアウトを分けたいが、URLには階層を足したくないという場面で使います。チームや機能の単位でフォルダを整理する用途も公式に挙げられています。
この挙動は実際に確認しました。app/(marketing)/pricing/page.tsxを置いてビルドし、next startで起動したサーバーに問い合わせたところ、/pricingは200を返し、/marketing/pricingは404。ビルド出力のルート表にも/pricingとしか出ません。
ただしURLへの影響が皆無というわけではないので、公式ドキュメントのCaveatsは押さえておいてください。別グループのルートが同じURLパスに解決する構成、たとえば(marketing)/about/page.jsと(shop)/about/page.jsはどちらも/aboutになるためエラーになります。またルートレイアウトを複数持つ場合、異なるルートレイアウト間の遷移はフルページリロードが発生します。
App RouterとPages Routerの違いと選択基準
データ取得・レンダリング・ルーティングの相違点
両者はディレクトリが違うだけでなく、データ取得の書き方とコンポーネントの実行場所が根本から異なります。主要な対応関係を並べます。
| 観点 | Pages Router | App Router |
|---|---|---|
| ディレクトリ | pages | app |
| 既定のコンポーネント | クライアント | サーバー |
| サーバー側取得 | getServerSideProps | コンポーネント内でawait |
| ビルド時取得 | getStaticProps | コンポーネント内でawait |
| 静的パス列挙 | getStaticPaths | generateStaticParams |
| メタデータ | next/head | Metadata API |
| 共通レイアウト | _app・_document | layout(入れ子可) |
| ルーターフック | next/router | next/navigation |
「既定のコンポーネント」の行は補足が要ります。Pages Routerのページもサーバーで事前描画されてからクライアントでハイドレートされるため、サーバー描画が無いわけではありません。違うのは、App Routerの既定がクライアントへJavaScriptを送らないサーバーコンポーネントである点です。実務上の差がいちばん出るのはレイアウトのほうで、Pages Routerの_app.jsは1つしか持てず入れ子にできませんでしたが、App Routerのlayoutはフォルダ階層ぶん重ねられます。管理画面のサイドバーを/admin配下だけで共有する、といった構成が設定なしで書けます。
App Routerを選ばない方がよい条件
新規プロジェクトならApp Routerで問題ありません。create-next-appもApp Routerを既定のテンプレートにしています。判断が要るのは既存プロジェクトのほうで、次の2条件のいずれかに当てはまるなら、今は移行しないほうが合理的です。
1つ目は、getServerSideProps中心のページが多く残っている場合。Next.js 16へのバージョンアップだけでも、paramsの同期アクセス廃止、middleware.tsの改称、Turbopack既定化が同時に降ってきます。ここにルーティング方式の変更まで重ねると、不具合が出たときにどの変更が原因か切り分けられません。先に16へ上げてPages Routerのまま動かし、落ち着いてからルート単位で移すべきです。App RouterとPages Routerは同一プロジェクトで併用できます。
2つ目は、ページ数が少なく、ネストしたレイアウトもストリーミングも使わない静的なサイトです。layout・loading・not-found・route・defaultという予約ファイル名を覚える学習コストに見合いません。
なお、Edgeランタイムでのミドルウェア処理への依存は、よく移行のハードルとして挙げられますが、ルーター選択の判断材料にはなりません。proxyとmiddlewareの規約はPages Router側のドキュメントにも同じように存在するため、どちらのルーターを使うかとは独立した論点です。16のアップグレード側の課題として別に片付けてください。
Pages Routerからの移行でつまずく3点
getServerSidePropsとgetStaticPropsの置き換え先
移行ガイドは「getServerSideProps, getStaticProps and getInitialProps have been replaced with a simpler API」と述べています。置き換え先は専用の関数ではなく、サーバーコンポーネント内でのawaitです。取得したデータをpropsとして返す必要はなく、コンポーネントの中で直接使います。getStaticPathsだけは後継の関数があり、generateStaticParamsに名前が変わりました。
next/headからMetadata APIへの移行
next/headはappディレクトリでは使えません。移行ガイドは「In the app directory, next/head is replaced with the new built-in SEO support」と明記しています。代わりにpageやlayoutからmetadataオブジェクトをエクスポートするか、動的に組み立てるならgenerateMetadata関数をエクスポートします。generateMetadataにも動的セグメントのparamsが渡るため、記事タイトルをURLのスラッグから引いてtitleに反映する、といった処理はここに書きましょう。
useRouterの参照先変更とnext/compat/router
同じuseRouterという名前でも、next/routerから取るものとnext/navigationから取るものは別物です。公式ドキュメントは「The useRouter hook imported from next/router is not supported in the app directory but can continue to be used in the pages directory」としています。移行時はnext/navigationへ切り替えたうえで、返り値の違いにも対応が必要です。新しいuseRouterはpathnameを返さずusePathnameを使い、queryも返さないためuseSearchParamsとuseParamsに分けて取得します。
両方のルーターで同じコンポーネントを共有している移行期間には、next/compat/routerからuseRouterを読む選択肢があります。ただし戻り値の型がNextRouter | nullである点に注意してください。App Router側ではnullが返るため、const { isReady, query } = useRouter()のような分割代入をそのまま書くとエラーになります。App Routerに寄せ切ったらnext/navigationへ更新しましょう。
Next.js 13時代の解説が16で通用しない4点
ここが古い記事をそのまま読むと事故る部分です。Next.js 16は2025年10月21日公開で、App Router周辺の前提がいくつも変わりました。前提要件も上がっており、Node.js 20.9以上、TypeScript 5.1以上、ブラウザはChrome・Edge・Firefoxが111以上、Safariが16.4以上を要求します。以下、4点のうち最初の1点と後半2点は手元の16.3.0で挙動を確認したもの、キャッシュの節は公式ブログとドキュメントの記述にもとづくものです。バージョンごとの変更点全体はNext.js 16とは?ReactベースのWeb開発フレームワーク最新版の概要や主要機能を詳しく紹介にまとめています。
paramsの同期アクセスがエラーなくundefinedになる罠
13時代のサンプルはparams.slugを直接読んでいました。16ではparamsがPromiseになっているため、この書き方では値を取り出せません。問題は、失敗の仕方が本番経路だけ静かなことです。
// 13時代の書き方をそのまま16に持ち込んだ場合
export default function Page({ params }: { params: { slug: string } }) {
return <h1>slug={String(params.slug)}</h1>
}
// next build → next start して /blog/hello を取得した実測結果
// ビルド終了コード: 0
// HTTPステータス: 200
// 実HTML: <h1>slug=<!-- -->undefined</h1> ← 表示は slug=undefined
ビルドは成功し、TypeScriptの検査も通り、本番サーバーのログにも警告は出ませんでした。それでも描画結果だけがundefinedになります。await paramsに直した版では同じURLがslug=helloを返しました。
救いは開発サーバーです。同じコードをnext devで動かすと、次のエラーが該当行つきで表示されます。
Error: Route "/blog/[slug]" used `params.slug`. `params` is a Promise and must be
unwrapped with `await` or `React.use()` before accessing its properties.
Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis
つまり開発中に一度でも該当ページを開いていれば気づけます。危ないのは、開発時に踏まなかった動的ルートがそのまま本番へ出ていくパターンです。動的セグメントを使っているページで値が空になる症状に出会ったら、まずここを疑ってください。
キャッシュ既定の反転とCache Components
13系のApp Routerは、fetchの結果を既定でキャッシュする挙動が「暗黙的すぎる」と批判されてきました。この既定が覆されたのはNext.js 15で、移行ガイドに「fetch requests are no longer cached by default」と明記されています。ルートハンドラーのGETも同時に既定キャッシュをやめました。13時代の記事を読むときは、この一点だけでキャッシュ周りの記述が丸ごと古いと考えて構いません。
16はさらに一歩進め、キャッシュを明示的に選ぶ仕組みを入れました。公式ブログは「caching with Cache Components is entirely opt-in. All dynamic code in any page, layout, or API route is executed at request time by default」と述べており、キャッシュしたい箇所に"use cache"ディレクティブを書く形になっています。有効化はnext.config.tsでcacheComponents: trueを設定します。実験的フラグだったexperimental.pprは削除され、experimental.dynamicIOはcacheComponentsへ改称されました。
あわせてrevalidateTag()の引数も変わり、第2引数にcacheLifeプロファイルを渡す形になりました。単一引数の呼び出しは非推奨で、公式ドキュメントは「It currently works if TypeScript errors are suppressed, but this behavior may be removed in a future version」としています。TypeScriptエラーを抑制しないと通らないため、古い記事のrevalidateTag('posts')という記述はそのままでは書けません。
middleware.tsからproxy.tsへの改称
middleware.tsはproxy.tsに置き換わりました。ファイル名を変え、エクスポートする関数名もproxyに変えるだけで、中のロジックはそのままで構いません。旧名のまま16でビルドすると次の警告が出ます。
⚠ The "middleware" file convention is deprecated. Please use "proxy" instead.
To migrate automatically, run:
npx @next/codemod@canary middleware-to-proxy .
Learn more: https://nextjs.org/docs/messages/middleware-to-proxy
警告が出るだけでビルドは通り、ルート表には「ƒ Proxy (Middleware)」として登録されました。ただしproxyのランタイムはnodejs固定で、edgeは選べません。移行ガイドも「If you want to continue using the edge runtime, keep using middleware」としており、Edge依存がある場合は旧名のまま残す判断になります。とはいえmiddleware.tsは非推奨で、公式ブログは将来のバージョンで削除すると予告しています。Edge依存が無いなら、動いているうちにcodemodで移しておくのが安全です。
Turbopack既定化とnext lintの削除
Turbopackは16で安定版になり、既定のバンドラーになりました。前掲のビルド出力でオプション無指定のまま「(Turbopack)」と表示されたとおりです。webpackを使い続けるならnext build --webpackのように明示します。注意したいのは、カスタムのwebpack設定を持ったままnext buildを走らせると、設定ミスを防ぐ目的でビルドが失敗する仕様になっている点。--webpackを付けるか、Turbopack向けの設定へ移すかを先に決めておく必要があります。
next lintは削除されました。手元で実行すると、コマンドとして解釈されず「lint」がプロジェクトのディレクトリ名として扱われ、Invalid project directory provided, no such directory: .../lintというエラーになります。ESLintやBiomeを直接呼ぶ形に変更が必要で、next buildもリントを実行しなくなりました。
細かい点ですが、既存プロジェクトにappを足す場合はpackage.jsonのtypeフィールドにも注意してください。"type": "commonjs"が入っているとTurbopackのビルドがモジュール形式の不一致で失敗します。create-next-appが生成するpackage.jsonにこのフィールドは無く、手作りのプロジェクトにnpm initの既定値が残っているときに踏む落とし穴です。
よくある質問
appディレクトリとpagesディレクトリは併用できますか?
併用できます。ただし同じURLパスを両方が持つとビルドが失敗します。手元の16.3.0でapp/blog/page.tsxとpages/blog.tsxを同時に置いたところ、App Router and Pages Router both match path: /blogというエラーでビルドが停止しました。重複しないパスであれば問題なく共存し、ビルド出力にもRoute (app)とRoute (pages)が別の表として並びます。段階移行するときは、移す対象のページをPages Router側から削除してからApp Router側に作る順序を守ってください。
Pages Routerは廃止されるのですか?
2026年8月時点で廃止の予告はありません。App Routerを安定版にした13.4の公式FAQで、Vercelはpages/の開発をバグ修正・改善・セキュリティパッチを含めて複数のメジャーバージョンにわたりサポートすると明言しています。実際、最新安定版16.3.0のドキュメントにもPages Router向けのセクションが残っています。ただし新機能はApp Router側に投入されるため、長期的にはApp Routerへ寄せる前提で計画を立てるのが妥当です。
App Routerでnext/headは使えますか?
使えません。appディレクトリではnext/headが組み込みのSEOサポートに置き換えられており、公式の移行ガイドもそう明記しています。代わりにpageやlayoutからmetadataオブジェクトをエクスポートしてtitleやdescriptionを指定します。URLのスラッグに応じてタイトルを変えるなど動的に組み立てる場合は、generateMetadata関数をエクスポートしてください。この関数にも動的セグメントのparamsが渡されます。
page.jsとpage.tsxはどちらで書くべきですか?
どちらでも動きます。公式のAPIリファレンスはpageに使える拡張子として.js・.jsx・.tsxを挙げており、JavaScriptのプロジェクトならapp/page.js、TypeScriptならapp/page.tsxという使い分けになります。TypeScriptでも.tsではなく.tsxを選ぶ理由は、pageがJSXを返すためです。JSXを含まないrouteのほうは.tsで書きます。なおファイル名pageの部分は予約語なので変更できません。
getServerSidePropsはApp Routerでどう書き換えますか?
対応する関数はなく、サーバーコンポーネントの中で直接awaitしてデータを取得します。コンポーネントをasync functionとして定義し、取得した値をそのまま描画に使う形です。propsとして返す手順は不要になりました。ビルド時に静的パスを列挙していたgetStaticPathsだけは後継関数があり、generateStaticParamsという名前に変わっています。なおfetchの結果はNext.js 15以降は既定でキャッシュされないため、キャッシュしたい箇所は明示的に指定してください。