Next.js 動的ルーティング完全攻略:基礎から Type-Safe な実装まで

はじめに
先週、Next.js プロジェクトのリファクタリング中に、私は発狂寸前でした。
ドキュメント通りに動的ルートを設定したはずなのに、何度クリックしても「404 Not Found」。コンソールには何のエラーも出ていません。
原因は、Next.js 14 App Router での「パラメータ取得方法の変更」でした。私は無意識に Pages Router 時代の useRouter を使って URL パラメータを取ろうとしていたのです。
Next.js のルーティングシステムは強力ですが、App Router でファイルシステムベースのルーティングが刷新されたため、混乱しやすいポイントが多々あります。「動的セグメント」「Catch-all」「Optional Catch-all」……これらをどう使い分けるべきか? そして、TypeScript でどうやって型安全にするのか?
この記事では、Next.js の動的ルーティングを基礎から応用まで解説します。Pages Router からの移行で躓きやすいポイントや、私が実際に使っている「型安全なパラメータ処理」の実装パターンも紹介します。
第一章:動的ルーティングの基礎
「動的ルート」とは?
例えばブログサイトを想像してください。記事ごとに /blog/hello-world、/blog/nextjs-guide といった URL が必要です。これら一つ一つに対してファイルを作るのは不可能です。
そこで登場するのが動的ルートです。一つのファイルで /blog/* 以下のすべてのアクセスを処理します。
基本的な実装:[slug]
App Router では、フォルダ名に角括弧 [] を使うことで動的セグメントを作成します。
app/
├── blog/
│ └── [slug]/
│ └── page.tsx ← これが動的ルートこの page.tsx は、/blog/foo や /blog/bar にマッチします。
パラメータは props.params として受け取ります。
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params
}: {
params: { slug: string }
}) {
return (
<div>
<h1>記事スラッグ: {params.slug}</h1>
</div>
)
}Pages Router との違い:
- Pages Router:
useRouter().query.slugまたはgetServerSidePropsの引数 - App Router: コンポーネントの
props.params(Server Component なので Hooks は使えません)
実践例:商品詳細ページ
/products/123 のようなページを作る場合:
// app/products/[id]/page.tsx
// 実際のデータフェッチ関数(モック)
async function getProduct(id: string) {
const products = [
{ id: '1', name: 'TypeScript 入門', price: 2980 },
{ id: '2', name: 'React 実践ガイド', price: 3980 }
]
return products.find(p => p.id === id)
}
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
// サーバーサイドで直接データを取得
const product = await getProduct(params.id)
if (!product) {
return <div>商品が見つかりません</div>
}
return (
<article>
<h1>{product.name}</h1>
<p>¥{product.price}</p>
</article>
)
}もし「商品が見つからない」場合に 404 ページを表示させたいなら、Next.js の notFound() 関数を使います。
import { notFound } from 'next/navigation'
// ...
if (!product) {
notFound() // これを実行すると app/not-found.tsx が表示される
}第二章:複雑なパスを扱う Catch-All
深い階層に対応する:[...slug]
ドキュメントサイトなどで、/docs/getting-started/installation のように階層が深くなる場合があります。
普通の [slug] では1階層しかマッチしませんが、Catch-All セグメント [...slug] を使うと、その下のすべての階層を配列として受け取ることができます。
app/
├── docs/
│ └── [...slug]/
│ └── page.tsxマッチング例:
/docs/a→params.slug = ['a']/docs/a/b→params.slug = ['a', 'b']/docs/a/b/c→params.slug = ['a', 'b', 'c']
// app/docs/[...slug]/page.tsx
export default function DocsPage({
params
}: {
params: { slug: string[] } // 注意:配列になります
}) {
const path = params.slug.join('/')
return <h1>現在のパス: {path}</h1>
}オプショナル Catch-All:[[...slug]]
Catch-All [...slug] は、パラメータがない場合(/docs)にはマッチしません。
ルートパス(/docs)も含めてマッチさせたい場合は、二重角括弧 [[...slug]] を使います。
マッチング例:
/docs→params.slug = undefined(または空配列)/docs/a→params.slug = ['a']
これを使えば、ドキュメントのトップページと各記事ページを1つのファイルで処理できます。
第三章:静的生成 generateStaticParams
動的ルートはデフォルトではリクエスト時にレンダリング(SSR)されますが、ブログ記事のように内容が変わらないページなら、ビルド時に静的 HTML を生成(SSG)した方が高速です。
そのために使うのが generateStaticParams です(Pages Router 時代の getStaticPaths に相当)。
// app/blog/[slug]/page.tsx
// ビルド時に実行され、生成すべきパスのリストを返す
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
// 返り値は params オブジェクトの配列
return posts.map((post) => ({
slug: post.slug,
}))
}
export default async function BlogPost({ params }) {
// ...
}使い分けの基準:
- 使うべき:ブログ、ニュース、製品詳細、ドキュメント(更新頻度が低い、全ユーザーに同じ内容)
- 使わない:検索結果、ユーザープロフィール(数が膨大)、リアルタイムデータ
大規模サイトでの戦略(ISR / On-demand)
記事が10万件ある場合、ビルド時に全部生成すると時間がかかりすぎます。
その場合は、一部(最新100件など)だけ generateStaticParams で返し、残りはリクエストがあった時に生成するように設定できます。
export const dynamicParams = true // デフォルト true:未生成のパスは初回アクセス時に生成
// false にすると、generateStaticParams にないパスは 404 になる第四章:TypeScript による型安全なパラメータ処理
params はいつでも string?
Next.js の仕様上、URL パラメータは常に文字列です。しかし、アプリ内では数値 ID (/products/123) として扱いたい場合が多々あります。
そのまま parseInt(params.id) としてもいいのですが、もしユーザーが /products/abc と入力したら? NaN になってバグの原因になります。
Zod を使ったランタイムバリデーション
私は Zod を使って、入り口(Page コンポーネント)でパラメータを厳格にチェックすることを推奨しています。
npm install zod// app/products/[id]/page.tsx
import { notFound } from 'next/navigation'
import { z } from 'zod'
// パラメータのスキーマ定義
const paramsSchema = z.object({
id: z.coerce.number().int().positive(), // 文字列を数値に変換し、正の整数かチェック
})
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
// 安全にパース
const parsed = paramsSchema.safeParse(params)
if (!parsed.success) {
// バリデーション失敗(例: /products/abc)なら 404
notFound()
}
const { id } = parsed.data // ここで id は number 型になる!
// これ以降は安心して数値として扱える
const product = await fetchProduct(id)
// ...
}このパターンのメリット:
- 型安全:
idが確実にnumberであることが保証される。 - バグ防止:不正な URL パラメータによるクラッシュや予期せぬ DB エラーを防げる。
- 明確なエラー処理:不正なリクエストは早期に 404 として弾ける。
実践テクニック:URL エンコード問題
日本語 URL (/blog/こんにちは) を使う場合、params.slug にはエンコードされた文字列(%E3%81...)が入ってくることがあります(環境によります)。
安全のため、使用前にデコードしておくと良いでしょう。
const slug = decodeURIComponent(params.slug)まとめ
Next.js の動的ルーティングは、[slug] の基本さえ押さえれば非常に直感的です。
しかし、本番レベルのアプリケーションでは、generateStaticParams によるパフォーマンス最適化や、Zod による入力値検証が欠かせません。
- 基本は
[slug]、階層が深いなら[...slug]。 - 静的コンテンツなら
generateStaticParamsで爆速化。 - パラメータは Zod でバリデーション して安全に使う。
これらを意識するだけで、あなたの Next.js アプリはより堅牢で、高速なものになるはずです。
Next.js 動的ルーティング実装フロー
パラメータ定義から静的生成、バリデーションまでのステップ
⏱️ Estimated time: 20 min
- 1
Step1: 動的セグメントの作成
`app/blog/[slug]/page.tsx` のように、フォルダ名を角括弧で囲んで作成します。これで `/blog/anything` へのアクセスをキャッチできます。 - 2
Step2: パラメータの受け取り
Page コンポーネントの `props` から `params` オブジェクトを受け取ります。TypeScript 型定義は `{ params: { slug: string } }` となります。 - 3
Step3: 静的パラメータの生成(SSG)
ビルド時にページを生成したい場合、`generateStaticParams` 関数をエクスポートし、可能なパラメータの配列(例: `[{ slug: 'a' }, { slug: 'b' }]`)を返します。 - 4
Step4: パラメータの検証
受け取った `params` を Zod などのライブラリで検証・変換します。数値が必要な場合やフォーマットが決まっている場合に、不正なアクセスを早期に遮断できます。
FAQ
getStaticPaths と generateStaticParams の違いは?
動的ルートでクエリパラメータ (?page=1) はどう取得しますか?
動的ルートのページタイトルを動的に変えるには?
catch-all ルートのパラメータ配列の順序はどうなりますか?
3 min read · 公開日: 2025年12月25日 · 更新日: 2026年1月22日
関連記事
Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践

Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践
Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド

Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド
Next.js ユニットテスト実践:Jest + React Testing Library 完全設定ガイド


コメント
GitHubアカウントでログインしてコメントできます