Next.js の App Router とは、app フォルダーの中にフォルダーとファイルを置くだけで URL と画面の構成が決まる、Next.js の標準のルーティング方式です。React Server Components(サーバー側で描画する部品)を前提にしており、データ取得・キャッシュ・フォーム送信までを、ひとつの考え方で書けるのが特徴です。

App Router の入門記事は多くありますが、Next.js 13.4 の頃の情報のままだったり、ルーティングだけで終わっていたりするものも見かけます。この記事は、2026年9月時点の最新版 Next.js 16.3.6 で実際に小さなブログアプリを作り、npm run build の出力(どのページが静的で、どれが動的か)や、よく出るビルドエラーの実物まで載せています。キャッシュの仕組みは Next.js 16 で大きく変わったため、公式ドキュメントに沿って「従来のモデル」と「Cache Components」の両方を、実行結果つきで整理しました。

Next.js App Router の早見表

やりたいこと App Router での書き方
ページを作る app/about/page.tsx → /about
全ページ共通の枠(ヘッダーなど) app/layout.tsx(ルートレイアウト。必須)
読み込み中の表示 loading.tsx(自動で Suspense になる)
エラー時の表示 error.tsx('use client' が必須)
404ページ not-found.tsx と notFound()
URLの一部を変数にする app/posts/[id]/page.tsx(params は Promise なので await)
ボタンや入力など、ブラウザで動く部品 ファイル先頭に 'use client'
データ取得 async の Server Component の中で await
フォーム送信・更新処理 Server Actions('use server')
APIを作る app/api/hello/route.ts に GET などを export
タイトル・説明文 export const metadata / generateMetadata
キャッシュ(Next.js 16の新方式) cacheComponents: true + 'use cache'
更新後にキャッシュを捨てる revalidatePath / revalidateTag(tag, 'max') / updateTag

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Next.js App Router 入門|v16対応・実際のビルド結果つき」

2026年9月時点のバージョンと、動作確認の環境

この記事は next 16.3.6(npm view next version で確認した最新版)、react 19.2.8、Node.js 22.22.2 で動作を確認しています。Next.js 16 の最低要件は Node.js 20.9 以上です。

Next.js 16(2025年10月)以降の主な変化は次のとおりです。古い記事を読むときは、ここが違う点に注意すると混乱しにくくなります。

項目 以前 Next.js 16 以降
params / searchParams / cookies() / headers() 同期でも読めた(15では一時的な互換あり) 必ず await する(同期アクセスは削除)
middleware.ts ミドルウェア proxy.ts(Proxy)に名称変更(機能は同じ)
revalidateTag('tag') 引数1つ 第2引数('max' など)が必要。1引数は非推奨
キャッシュの新方式 実験的な dynamicIO / useCache cacheComponents: true と 'use cache'
next lint / バンドラー next lint あり・webpack next lint は削除、Turbopack が標準

プロジェクトを作る

npx create-next-app@latest nextapp --ts --app --eslint --tailwind --no-src-dir --import-alias "@/*" --use-npm --yes
cd nextapp
npm run dev

--app を付けると App Router のプロジェクトになります(現在は既定でも App Router です)。作成直後の app/ には layout.tsx・page.tsx・globals.css があり、AIコーディングエージェント向けの AGENTS.md も生成されていました。生成された layout.tsx は LayoutProps<"/"> という型を使っていました。Next.js 16 では、ルートの型(PageProps<"/posts/[id]"> など)がビルド時に自動生成され、params の型を手で書かなくてよくなっています。

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

App Router では「フォルダーが URL の区切り、page.tsx がその URL の画面」です。page.tsx がないフォルダーは URL として公開されないので、部品やユーティリティを同じフォルダーに置いても問題ありません。

特別なファイルの一覧

ファイル 役割 ポイント
page.tsx その URL の画面 これがあるフォルダーだけが公開される
layout.tsx 子ページを包む共通の枠 ページを移動しても状態が保たれ、再描画されない
loading.tsx 読み込み中の表示 page を自動で <Suspense> で包む
error.tsx エラー時の表示 Client Component 必須('use client')
not-found.tsx 404の表示 notFound() を呼ぶと表示される
route.ts API(Route Handler) 同じフォルダーに page.tsx と同居できない

ルートレイアウトとページ

// app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: { default: "サンプルブログ", template: "%s | サンプルブログ" },
};

export default function RootLayout({ children }: LayoutProps<"/">) {
  return (
    <html lang="ja">
      <body>{children}</body>
    </html>
  );
}

ルートレイアウトには <html> と <body> を必ず書きます。ページ間の移動には <a> ではなく next/link の <Link> を使うと、画面全体を読み直さずに移動でき、表示範囲に入ったリンク先を事前に読み込む(プリフェッチ)こともできます。

loading・error・not-found

// app/posts/error.tsx(loading.tsx は <p>読み込み中…</p> を返すだけ)
"use client";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return <button onClick={() => reset()}>再試行({error.message})</button>;
}

error.tsx はボタンの onClick などブラウザ側の処理を持つため、'use client' が必要です。not-found.tsx も同じように置くだけで、notFound() を呼んだときに表示されます。なお error.tsx は同じ階層の layout.tsx のエラーは受け止めず、1つ上の階層の error.tsx が担当します。

動的ルート

// app/posts/[id]/page.tsx
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getPost, getPosts } from "@/app/lib/data";
import LikeButton from "@/app/components/LikeButton";

// ビルド時に作っておくURLの一覧
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((p) => ({ id: p.id }));
}

// ページごとのタイトル
export async function generateMetadata({ params }: PageProps<"/posts/[id]">): Promise<Metadata> {
  const { id } = await params;
  const post = await getPost(id);
  return { title: post?.title ?? "見つかりません" };
}

export default async function PostPage({ params }: PageProps<"/posts/[id]">) {
  const { id } = await params;          // Next.js 16 では必ず await
  const post = await getPost(id);
  if (!post) notFound();                // not-found.tsx を表示
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
      <LikeButton />
    </article>
  );
}
フォルダー名 マッチするURL params の中身
[id] /posts/1 { id: "1" }
[...slug] /docs/a/b { slug: ["a", "b"] }(1階層以上)
(marketing) URLに含まれない ルートグループ。レイアウトを分けたいとき

generateStaticParams で返した URL はビルド時に HTML になり、それ以外の ID は初回アクセス時に作られます(dynamicParams の既定が true のため)。

Server Components と Client Components

App Router では、何も書かなければすべて Server Component です。サーバーで描画され、その部品の JavaScript はブラウザに送られません。ブラウザで動かしたい部品だけ、ファイル先頭に 'use client' を書きます。

Server Component(既定) Client Component('use client')
動く場所 サーバー(ビルド時またはリクエスト時) サーバーで初回HTMLを作り、ブラウザで動く
async / await でデータ取得 できる コンポーネント自体は async にできない
DBや秘密のAPIキーへのアクセス できる してはいけない(ブラウザに届く)
useState / useEffect 使えない 使える
onClick などのイベント 使えない 使える
// app/components/LikeButton.tsx
"use client";

import { useState } from "react";

export default function LikeButton() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount((c) => c + 1)}>いいね {count}</button>;
}

'use client' は「境界」に1回書く

'use client' は、そのファイルと、そこから import されるファイルをまとめてクライアント側にする「境界線」です。すべてのファイルに書く必要はありません。考え方のコツは次のとおりです。

  • ページやレイアウトは Server Component のままにする
  • ボタン・フォーム入力・タブなど、操作が必要な末端の部品だけを Client Component に切り出す
  • Server Component から Client Component へは props で値を渡せる(関数など、シリアライズできない値は渡せない。Server Actions は例外)

実際に出るビルドエラー(2種類)

'use client' を書き忘れて useState を使うと、ビルドは次のエラーで止まります(LikeButton.tsx の先頭行を消して npm run build した実際の出力の抜粋)。

Error: Turbopack build failed with 1 error:
./app/components/LikeButton.tsx:1:10
Error: You're importing a module that depends on `useState` into a React Server Component module. This API is only available in Client Components. To fix, mark the file (or its parent) with the `"use client"` directive.

Import trace:
  Server Component:
    ./app/components/LikeButton.tsx
    ./app/posts/[id]/page.tsx

データ取得のファイルの先頭に import "server-only";(npm i server-only)を書いておくと、Client Component から import したときに You're importing a module that depends on "server-only" into a React Client Component module. というエラーでビルドが止まります。DB接続や APIキーを扱うファイルに入れておくと、秘密の情報がブラウザ向けのコードに混ざるのを防げます。

データ取得:async の Server Component で await する

// app/lib/data.ts
import "server-only";

export type Post = { id: string; title: string; body: string };
const posts: Post[] = [
  { id: "1", title: "App Routerを始める", body: "page.tsxを置くとページになります。" },
  { id: "2", title: "Server Components", body: "サーバーで描画されます。" },
];

export async function getPosts(): Promise<Post[]> {
  await new Promise((r) => setTimeout(r, 300)); // DBアクセスの代わり
  return posts;
}
// getPost(id)・addPost(title, body) も同じファイルに定義

Server Component では、useEffect も API ルートも使わずに、コンポーネントの中で直接 await してデータを取れます。外部 API なら await fetch(...)、DBなら ORM の関数をそのまま呼びます。

互いに依存しないデータが複数あるなら Promise.all で同時に取り、遅いデータは <Suspense> で囲んで後から表示すると、画面の骨格を先に出せます。

キャッシュと再検証:Next.js 16 で2つのモデルがある

Next.js 16 のキャッシュは、「従来のモデル」と、cacheComponents: true で有効になる「Cache Components」の2つがあり、公式ドキュメントもページが分かれています。create-next-app で作った直後は next.config.ts が空なので、従来のモデルで動きます。

従来のモデル(既定) Cache Components(cacheComponents: true)
fetch の既定 キャッシュしない(15から) キャッシュしない。必要なら 'use cache'
キャッシュの指定 fetch(url, { cache: 'force-cache' })、next: { revalidate: 3600 }、unstable_cache 関数やコンポーネントの先頭に 'use cache' + cacheLife
cookies() などを使ったとき ルート全体が動的になる その部分だけ <Suspense> で後から流す
描画のされ方 静的か動的か、ページ単位 部分プリレンダー(PPR)が標準

従来のモデルでのビルド結果

上のサンプル(/api/hello と、headers() を読む /time を追加)を npm run build した実際の出力です(途中の行は省略)。

▲ Next.js 16.3.6 (Turbopack)
✓ Compiled successfully in 12.8s

Route (app)
┌ ○ /
├ ○ /_not-found
├ ƒ /api/hello
├ ○ /posts
├   /posts/[id]
│ ├ ● /posts/1
│ └ ● /posts/2
└ ƒ /time

○  (Static)   prerendered as static content
●  (SSG)      prerendered as static HTML (uses generateStaticParams)
ƒ  (Dynamic)  server-rendered on demand
記号 意味 このアプリでは
○ Static ビルド時にHTMLを作った /、/posts
● SSG generateStaticParams でビルド時に作った /posts/1、/posts/2
ƒ Dynamic リクエストのたびにサーバーで作る headers() を読む /time、/api/hello

ここで押さえておきたいのは、getPosts() はキャッシュ指定をしていないのに /posts が ○(静的)になっている点です。リクエスト時の情報(cookies()、headers()、searchParams など)を使わないページは、ビルド時にまるごと HTML にされます。つまり 「データ取得がキャッシュされない」ことと「ページが静的に作られる」ことは別の話 で、データを更新したら revalidatePath などでページを作り直す必要があります。

next start で確かめると、/posts の応答には Cache-Control: s-maxage=31536000、/time には private, no-cache, no-store, ... が付いていました。

Cache Components を有効にしたときのビルド結果

next.config.ts に cacheComponents: true を足して同じアプリをビルドすると、/time でビルドが止まりました。

Error: Route "/time": Next.js encountered uncached or runtime data during prerendering.

Ways to fix this:
  - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
  - [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` (does not apply to `connection()`)
  - [block] Set `export const instant = false` to allow a blocking route

Cache Components では「リクエスト時の情報は <Suspense> の中で読む」がルールです。次のように書き直すと通ります。

// app/time/page.tsx(Cache Components 用)
import { Suspense } from "react";
import { headers } from "next/headers";

async function UserAgent() {
  const ua = (await headers()).get("user-agent") ?? "unknown";
  return <p>あなたのブラウザ:{ua}</p>;
}

export default function TimePage() {
  return (
    <Suspense fallback={<p>読み込み中…</p>}>
      <UserAgent />
    </Suspense>
  );
}

さらに、データ取得の関数に 'use cache' を付けます。

// app/lib/data.ts(Cache Components 用の getPosts)
import { cacheLife, cacheTag } from "next/cache";

export async function getPosts(): Promise<Post[]> {
  "use cache";
  cacheLife("hours");   // 1時間ごとに再検証
  cacheTag("posts");    // あとで updateTag("posts") で捨てられるように
  await new Promise((r) => setTimeout(r, 300));
  return posts;
}

このときのビルド出力です。

- Cache Components enabled

Route (app)        Revalidate  Expire
┌ ○ /
├ ○ /_not-found
├ ƒ /api/hello
├ ○ /posts                 1h      1d
├   /posts/[id]
│ ├ ◐ /posts/[id]
│ ├ ○ /posts/1
│ └ ○ /posts/2
└ ◐ /time

◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content

/time は ◐(部分プリレンダー)になり、「読み込み中…」はビルド時の HTML に含まれ、ブラウザ情報の部分だけがリクエスト時に流れてきます。/posts には cacheLife("hours") の値(再検証 1h、期限 1d)が表示されました。cacheLife には seconds・minutes・hours・days・weeks・max のプロファイルがあり、たとえば hours は「1時間で再検証、1日で期限切れ」です(公式ドキュメントより)。

再検証の関数の使い分け

関数 呼べる場所 動き 向いている場面
revalidatePath('/posts') Server Actions、Route Handlers そのパスのキャッシュを捨てる どのタグか分からないとき
revalidateTag('posts', 'max') Server Actions、Route Handlers 古い内容を返しつつ裏で作り直す ブログ・商品一覧など、少し遅れてよいもの。CMSのWebhookから
updateTag('posts') Server Actions のみ すぐに期限切れにする 自分が投稿した内容を、直後に自分で見る場面

公式ドキュメントでは、パス単位よりタグ単位の再検証がすすめられています(必要な範囲だけを捨てられるため)。

Server Actions:フォーム送信をサーバーの関数で受ける

Server Actions は、'use server' を付けた async 関数を、<form action={...}> に直接渡せる仕組みです。API ルートを作って fetch で POST する、という手順を省けます。

// app/posts/actions.ts
"use server";

import { revalidatePath } from "next/cache";
import { addPost } from "@/app/lib/data";

export async function createPost(formData: FormData) {
  const title = String(formData.get("title") ?? "").trim();
  if (!title) return;
  await addPost(title, "本文は未入力です");
  revalidatePath("/posts");   // Cache Components なら updateTag("posts")
}

ページ側は、フォームに <form action={createPost}> と渡すだけです(入力欄 name="title" と送信ボタンを置く)。

本番ビルド(next start)をブラウザ(Playwright)で操作して、フォーム送信後に一覧が更新されることを確認しました。従来のモデル(revalidatePath)と Cache Components(updateTag)のどちらでも同じ結果です。

before: [ 'App Routerを始める', 'Server Components' ]
after:  [ 'App Routerを始める', 'Server Components', 'Server Actionで追加' ]
いいね 2   ← /posts/1 の Client Component を2回クリックした結果

Server Actions を使うときの注意点です。

  • Server Actions は、画面からだけでなく POST リクエストで直接呼べます。関数の中で必ずログイン確認と権限確認をする(公式ドキュメントでも警告されています)
  • 入力値は FormData から取り出し、サーバー側で検証する(zod などのスキーマ検証ライブラリが便利)
  • Client Component から呼ぶなら、ファイル先頭に 'use server' を書いた別ファイルにまとめる
  • 送信中の表示やエラー表示は、React の useActionState / useFormStatus を使う

Route Handlers:API を作る

// app/api/hello/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const name = searchParams.get("name") ?? "guest";
  return Response.json({ message: `hello, ${name}` });
}
$ curl "http://localhost:3456/api/hello?name=taro"
{"message":"hello, taro"}

$ curl -X POST -o /dev/null -w "%{http_code}\n" http://localhost:3456/api/hello
405

GET・POST・PUT・PATCH・DELETE・HEAD・OPTIONS を関数名で export します。定義していないメソッドには、上のとおり自動で 405 Method Not Allowed が返りました。

使い分け Server Actions Route Handlers
呼ぶのは 自分のアプリのフォームやボタン 外部サービス、スマホアプリ、Webhook
HTTPメソッド POST のみ 自由に定義できる
向いている処理 画面からの登録・更新・削除 公開API、Webhook受信、ファイル配信

自分のアプリの画面からの更新は Server Actions、外から呼ばれる入口は Route Handlersと分けると整理しやすくなります。なお、Server Component から自分の Route Handler を fetch する必要はありません。データ取得の関数を直接呼べば済みます。

metadata:タイトルと説明文

固定のタイトルは export const metadata = { title: "記事一覧" }、URL によって変わるタイトルは前述の generateMetadata で返します。ルートレイアウトの title.template("%s | サンプルブログ")と組み合わせた、実際の出力です。

/posts    → <title>記事一覧 | サンプルブログ</title>
/posts/1  → <title>App Routerを始める | サンプルブログ</title>

同じ URL への fetch は1回の描画の中で自動的にまとめられるため、generateMetadata とページ本体で同じデータを取っても二重には通信しません(DBを直接呼ぶなら React の cache() で包む)。OGP画像は opengraph-image.tsx、サイトマップは sitemap.ts を置くだけで作れます。

404 のステータスコードに注意(検証で分かったこと)

存在しない /posts/99 にアクセスすると、not-found の画面は表示されましたが、HTTPステータスは 200 でした。

$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3456/posts/99
200
$ curl -s http://localhost:3456/posts/99 | grep -o '<meta name="robots"[^>]*>'
<meta name="robots" content="noindex"/>

これは仕様です。app/posts/loading.tsx があるため、応答は Suspense のストリーミングで始まり、先に 200 OK を返しています。その後で notFound() が呼ばれても、ステータスを変更できません。代わりに <meta name="robots" content="noindex"> が自動で入り、検索エンジンに登録されないようになっています(公式ドキュメントの streaming ガイド、loading.js のページに記載)。一方、どこにも一致しない /nothing は 404 でした。

本物の 404 ステータスが必要な場合は、ストリーミングが始まる前(proxy.ts など)で存在確認をします。

Pages Router との違い

項目 Pages Router(pages/) App Router(app/)
ページの定義 pages/about.tsx app/about/page.tsx
共通レイアウト _app.tsx / _document.tsx layout.tsx(入れ子にできる)
データ取得 getServerSideProps / getStaticProps async Server Component で await
部品の既定 すべて Client Component すべて Server Component
API pages/api/*.ts app/**/route.ts
フォーム送信 API ルートへ fetch Server Actions
読み込み中・エラー 自分で実装 loading.tsx / error.tsx
ルーターのフック next/router next/navigation(useRouter、usePathname など)

Pages Router も引き続きサポートされており、pages/ と app/ は同じプロジェクトに共存できるため、移行はページ単位で進められます。新しく作るなら、公式ドキュメントの中心である App Router から始めるのがおすすめです。

よくあるつまずきと対処

症状 原因 対処
useState のビルドエラー Server Component でフックを使った 操作する部品を切り出して 'use client'
params.id が undefined や型エラー Next.js 16 では params が Promise const { id } = await params
データを更新しても画面が古い ページがビルド時に静的生成されている revalidatePath / updateTag を呼ぶ
middleware.ts が効かない(16へ更新後) 名称が Proxy に変わった proxy.ts にし、関数名を proxy に(codemod あり)

よくある質問

Q. Cache Components は有効にしたほうがいいですか? A. 新しく作るアプリなら、公式ドキュメントの「Caching」ページが Cache Components を前提に書かれているので、有効にして始めると資料と食い違いにくくなります。既存アプリは、ビルドエラーで指摘されたルートを1つずつ直しながら移行します。

Q. fetch は自動でキャッシュされますか? A. Next.js 15 以降、fetch は既定でキャッシュされません。ただし従来のモデルでは、リクエスト時の情報を使わないページはビルド時に丸ごと静的化されるため、取得結果が HTML に固定されます(この記事の /posts がその例)。

Q. next dev と next build で動きが違うのはなぜですか? A. 開発中はページが毎回描画され、キャッシュされないためです(公式ドキュメントに記載)。キャッシュや静的・動的の判定は、npm run build の出力と npm start で確認するのが確実です。

まとめ

  • App Router は フォルダーが URL、page.tsx が画面。layout・loading・error・not-found を置くだけで役割が決まる
  • 何も書かなければ Server Component。操作が必要な末端の部品だけ 'use client' にする
  • データは async の Server Component で await して取る。秘密を扱うファイルには server-only
  • Next.js 16 では params などは必ず await、middleware は proxy に
  • キャッシュは 従来のモデル と Cache Components('use cache') の2つ。どちらかを意識して書く
  • 更新は Server Actions、外部向けの API は Route Handlers。再検証は revalidatePath・revalidateTag(tag, 'max')・updateTag を使い分ける
  • npm run build の ○ ● ◐ ƒ を見ると、各ページがどう配信されるかが分かる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Next.js App Router 入門|v16対応・実際のビルド結果つき」

株式会社RJCでは、Next.js のような進化の速いフレームワークも、公式ドキュメントと実際のビルド結果を確かめながら、仲間と一緒に学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?