Next.js App Router のファイルシステムベースルーティング その1

Next.js App Router のファイルについて迷わないように、ファイルシステムベースルーティングについてまとめました。

※経路を表す単語 Route と根を表す単語 Root のカタカナ表記はどちらも「ルート」となるので、混同を防ぐためこれらの単語は英語で書く場合があります。

App Router とは

Next.js のルーティングは Pages RouterApp Router の二種類の方法があります。 Pages Router は以前の Next.js からあるルーターで、 SSR (Server-Side Rendering) をする際には Next.js の独自仕様である getServerSideProps() 関数を export し、データフェッチを行っていました。対して App Router は Next.js 13.4 でリリースされた新しいルーターで、 RSC (React Server Components) や Suspense など React の新しい標準機能を使用して、データフェッチ処理を書くことができます。また、サーバーサイドでデータフェッチが完了したコンポーネントから順次クライアントにコンポーネントをストリーミングすることもできます。 React の新機能を利用できるため、 Pages Router から App Router への移行を推奨しています。新しいプロジェクトでは App Router で書くようにしましょう。

The Pages Router is still supported in newer versions of Next.js, but we recommend migrating to the new App Router to leverage React's latest features. Introduction: Pages Router | Next.js

ファイルシステムベースのルーティング

npx create-next-app@latest で Next.js プロジェクトを作成します。途中で Would you like to use App Router? (recommended) と聞かれるので Yes を選択しましょう。

プロジェクトの Root に app ディレクトリがあります。この中のファイル構造が、アプリケーションの Route と対応しています。初期状態は以下の通りです。

$ tree app
app
├── favicon.ico
├── globals.css
├── layout.tsx
└── page.tsx

page.tsx とディレクトリ

page.tsx は各ページを定義します。 / へのアクセスがあったときは app ディレクトリ内の page.tsx が、 /about へのアクセスがあったときは about/page.tsx がそれぞれ使われます。ディレクトリを作ってその中に page.tsx を書けば、そのパスへのページ Route が作成されます。

app
├── about
│   ├── contact
│   │   └── page.tsx  --> /about/contact
│   └── page.tsx      --> /about
├── favicon.ico
├── globals.css
├── help
│   └── page.tsx      --> /help
├── layout.tsx
└── page.tsx          --> /

React コンポーネントを page.tsx 内で default export すると、そのコンポーネントが表示されます。

export default function Home() {
  return <div>Hi!</div>;
}

layout.tsx

page.tsx では個別のページを定義していました。実際の Web ページでは、ヘッダーやナビゲーションメニュー、フッターなど各ページで共通の要素がよくあります。これらの要素を毎回 page.tsx に書いていられないので、共通部分を layout.tsx に書けるようになっています。

create-next-app は、 app ディレクトリ内にデフォルトの layout.tsx を作成します。 app ディレクトリ直下の layout.tsx は必須ファイルで、すべてのページはこのレイアウトを使用します。

// 一部省略
export default function RootLayout({
  children
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className="antialiased">
        {children}
      </body>
    </html>
  );
}

children prop が、 page.tsx で export されたコンポーネントとなります。以下のような感じでページがレンダリングされます。

<RootLayout>
  <Page />
</RootLayout>

また、 layout.tsx はネストすることができます。

app
├── about
│   ├── contact
│   │   └── page.tsx  --> /about/contact  (app/layout.tsx と app/about/layout.tsx 適用)
│   ├── layout.tsx
│   └── page.tsx      --> /about  (app/layout.tsx と app/about/layout.tsx 適用)
├── favicon.ico
├── globals.css
├── help
│   └── page.tsx      --> /help  (app/layout.tsx 適用)
├── layout.tsx
└── page.tsx          --> /  (app/layout.tsx 適用)

ネストされたレイアウトは、 Root から順にネストされます。例えば、上記の app/about/page.tsx は以下のような感じでページがレンダリングされます。

<RootLayout>      {/* app/layout.tsx */}
  <AboutLayout>   {/* app/about/layout.tsx */}
    <Page />      {/* app/about/page.tsx */}
  </AboutLayout>
</RootLayout>

ヘッダーやフッターなど全ページで共通のものほど親のレイアウトに書き、設定画面のサイドメニューなど特殊化されているものは子のレイアウトに書くようにしましょう。

(directory) ディレクトリ

丸括弧がついたディレクトリは、 Route をグループ化します。 URL パスには影響しません。

例えば、 app/(lp)/about/page.tsx に書いたページは、 /about にアクセスすると見ることができます。 /(lp)/about ではないことに注意です。

app
├── (lp)
│   └── about
│       └── page.tsx  --> /about  (app/layout.tsx のみ適用!!!)
├── about
│   ├── contact
│   │   └── page.tsx  --> /about/contact  (app/layout.tsx と app/about/layout.tsx 適用)
│   └── layout.tsx
├── favicon.ico
├── globals.css
├── help
│   └── page.tsx      --> /help  (app/layout.tsx 適用)
├── layout.tsx
└── page.tsx          --> /  (app/layout.tsx 適用)

ディレクトリ構造は分かれているので、レイアウトをそれぞれ別にすることができます。上記の例では、 app/(lp)/about/page.tsx には Root レイアウト (app/layout.tsx) のみが適用されます。「ほとんどのページで共通のレイアウトを使うけど、このページだけ別のレイアウトにしたい」といった時に使いましょう。これを活用して、複数の Root レイアウトを持つサイトも構成できます。

レイアウトを分けることがなくても、単にファイル構造の整理のためだけに丸括弧ディレクトリを使用しても良いと思います (認証画面は (Auth) 、設定画面は (Config) として分けて整理するなど) 。

もちろん、同一の URL パスで複数の page.tsx がある場合 (app/abc/page.tsxapp/(def)/abc/page.tsx が同時に存在するなど) はエラーになります。どれを使うのか判断できないためです。

_directory ディレクトリ

アンダースコアで始まるディレクトリは、ルーティングから除外されます。例えば、 app/_components/page.tsx に書いたとしても /_components への Route は作成されません。関連するコンポーネントなどを近くに置く際に使用しましょう。

[directory] ディレクトリ

角括弧がついたディレクトリは、ダイナミックルートを表します。 URL パスの変数のようなものです。 URL パスセグメントちょうど 1 個にマッチします。

例えば、 app/users/[username]/page.tsx に書いたページは、 /users/alice/users/bob などにアクセスすると見ることができます。 alicebob などの部分は page.tsx のコンポーネントのパラメータで受け取ることができます。

export default async function Page({
  params
}: {
  params: Promise<{ username: string }>;
}) {
  const { username } = await params;
  return <div>User: {username}</div>;
}

ダイナミックルートはパスの中で複数回出てきても大丈夫です。 app/[username]/status/[id]/page.tsx を書けば、 { params: Promise<{ username: string; id: string }> } として受け取れます。あとはページコンポーネントでデータフェッチするなり好きに処理しましょう。

ダイナミックルートにマッチする通常ページがある場合、通常ページが優先されます。例えば、 app/users/list/page.tsxapp/users/[username]/page.tsx が両方存在する場合、 /users/list にアクセスすると app/users/list/page.tsx が使用されます。

ちなみに、 Next.js 15 以降では paramsPromise で包まれます。 Next.js 14 以前は Promise ではなく、同期的に取得するようになってました。後方互換性を保つため現在は同期的・非同期的どちらでも params が取得できるようになっていますが、今後は非同期的に書くようにしましょう。

In version 14 and earlier, params was a synchronous prop. To help with backwards compatibility, you can still access it synchronously in Next.js 15, but this behavior will be deprecated in the future. Routing: Dynamic Routes | Next.js

[...directory] ディレクトリと [[...directory]] ディレクトリ

... がついたダイナミックルートは、 URL パスセグメント 1 個以上 にマッチします。複数回マッチするので、階層化されたデータをそのまま URL パスとして表現するといった場合に使えます。 app/users/[username]/data/[...path]/page.tsx/users/alice/data/my-folder/my-data とマッチします。

params には配列として格納されます。例えば、 { params: Promise<{ username: string; path: string[] }> } といった型になります。

[[...directory]] のように二重角括弧の場合は、 URL パスセグメント 0 個以上 にマッチします。オプショナルなので params の型は { params: Promise<{ path?: string[] }> } といった Optional 型になります (? が追加されています) 。

route.ts

今までは page.tsx を配置することによるページの作成を見ていきました。 page.tsx ではなく route.ts を書くと、ページではなくルートハンドラーを作成することができます。ルートハンドラーは RequestResponse を直接扱うことができます。主に API Route を作るときに使用します。

export async function GET(_request: Request) {
  const date = new Date().toDateString();
  return Response.json({ date });
}

このコードを app/api/date/route.ts に書き、 /api/date を GET すると以下のような JSON レスポンスが返ってきます。

{"date":"Wed Apr 02 2025"}

GET 関数を named export すると HTTP GET メソッドに対応したハンドラーが作られます。同様に POST PUT PATCH DELETE HEAD OPTIONS 関数を export するとそれぞれの HTTP メソッドに対応できます。

もちろん、ダイナミックルートも使用することができます。第 2 引数に渡ってきます。

export async function GET(
  request: Request,
  { params }: { params: Promise<{ username: string }> }
) {
  const { username } = await params;
  // ...
}

もちろん、 route.ts と同じ URL パスに page.tsx が存在する場合、エラーになります。

おわり

今回は Next.js App Router ファイルシステムベースルーティングの基本を紹介しました。ここまでの機能でも十分に作れますが、 Parallel Routes や Intercepting Routes を使用すると、データフェッチを伴う動的なページの読み込み動作を改善したり、モーダルを作る際に便利だったりと、より高度なものを作れます。次回以降はこれらの機能を紹介します。

参考文献