01 Introduction to App Router
Since Next.js 13, the framework introduced the App Router: URLs are described using files and folders under the app/ directory, and pages and data fetching logic are organized based on React Server Components by default.
Compared to Pages Router (pages/)
- Route entries are driven by convention files like
page.tsx/layout.tsx, rather than a singlepages/*.tsx. - Layouts can be nested, sharing UI and loading states; data fetching is more naturally tied to the component tree (including server components).
- This chapter focuses solely on App Router; if legacy projects still use Pages Router, consult the official migration guide for gradual migration.
Key Point
Next.js 15 defaults to App Router as the mainstream approach; new projects should prioritize the app/ directory to stay aligned with official documentation and ecosystem examples.
02 File-System Routing Basics
In App Router, page.tsx (or page.js) declares the page UI for that path. The directory hierarchy maps directly to the URL hierarchy (under the app root).
Common mappings (using src/app as example):
src/app/page.tsx→ site root/src/app/about/page.tsx→/aboutsrc/app/blog/page.tsx→/blog
Minimal working homepage example:
// src/app/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "首页",
description: "站点首页",
};
export default function HomePage() {
return (
<main className="min-h-screen p-8">
<h1 className="text-3xl font-bold">欢迎来到首页</h1>
<p className="mt-4 text-gray-600">这是由 app/page.tsx 渲染的路由 /</p>
</main>
);
}
Key Point
Only directories that contain page.tsx (or page.js) become accessible "route segments"; a folder without a page file typically cannot be directly accessed (can be used for grouping, layouts, or private modules).
03 Layouts
layout.tsx wraps sibling and descendant routes, used for placing navbars, sidebars, theme containers, etc. When navigating between child routes, layouts remain mounted by default, making them suitable for global UI or context that shouldn't reset with page changes (note the difference from "templates" below).
Root layout (must include <html> and <body>):
// src/app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: { default: "我的站点", template: "%s | 我的站点" },
description: "根布局提供全站 HTML 外壳",
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="zh-CN">
<body className="antialiased">{children}</body>
</html>
);
}
Nested layout example (e.g., dashboard subtree sharing a sidebar):
// src/app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen">
<aside className="w-56 border-r p-4">侧栏</aside>
<section className="flex-1 p-6">{children}</section>
</div>
);
}
// src/app/dashboard/page.tsx → URL: /dashboard
export default function DashboardHome() {
return <h1>控制台首页</h1>;
}
template.tsx vs layout.tsx
Both can wrap child routes, but template remounts on every navigation (internal state resets, useEffect runs again), suitable for entry animations or forced subtree refresh scenarios; layout stays mounted during navigation, suitable for persistent shells.
04 Dynamic Routes
Folder names with square brackets denote dynamic segments. In Next.js 15, within server-side pages/layouts, params (and searchParams) are Promises that need to be awaited inside async components.
Single dynamic segment [slug]
// src/app/posts/[slug]/page.tsx
type Props = { params: Promise<{ slug: string }> };
export default async function PostPage({ params }: Props) {
const { slug } = await params;
return (
<article>
<h1>文章:{slug}</h1>
</article>
);
}
Catch-all [...slug]
// src/app/docs/[...slug]/page.tsx
type Props = { params: Promise<{ slug: string[] }> };
export default async function DocsPage({ params }: Props) {
const { slug } = await params; // 例如 /docs/a/b → ['a','b']
return <pre>{JSON.stringify(slug, null, 2)}</pre>;
}
Optional catch-all [[...slug]]
// src/app/wiki/[[...slug]]/page.tsx
type Props = { params: Promise<{ slug?: string[] }> };
export default async function WikiPage({ params }: Props) {
const { slug } = await params;
const segments = slug ?? [];
return <p>段落:{segments.join(" / ") || "(根)"}</p>;
}
Comparison of three patterns
| Pattern | Directory Example | URL Example | params (after await) |
|---|---|---|---|
[slug] |
app/posts/[slug] |
/posts/hello |
{ slug: 'hello' } |
[...slug] |
app/docs/[...slug] |
/docs/api/auth |
{ slug: ['api','auth'] } |
[[...slug]] |
app/wiki/[[...slug]] |
/wiki or /wiki/a/b |
{ slug?: string[] }, may be undefined at root |
05 Route Groups & Special Files
Parenthesized folder names (segment) are route groups: used only for organizing code and sharing layouts, and do not appear in the URL. For example, app/(marketing)/page.tsx still corresponds to /.
src/app/
(marketing)/
layout.tsx # 营销区布局
page.tsx # /
(app)/
dashboard/
page.tsx # /dashboard
loading.tsx
Shows a loading UI for the corresponding route segment, wrapped by the framework with Suspense; suitable for lists and slow data placeholders.
// src/app/dashboard/loading.tsx
export default function Loading() {
return <p className="animate-pulse">加载中…</p>;
}
error.tsx
Error boundary, must be a client component, receives error and reset.
// src/app/dashboard/error.tsx
"use client";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>出错了</h2>
<button type="button" onClick={() => reset()}>
重试
</button>
</div>
);
}
not-found.tsx
Displayed when notFound() is called or no route matches.
// src/app/not-found.tsx
import Link from "next/link";
export default function NotFound() {
return (
<div>
<h1>404</h1>
<p>页面不存在</p>
<Link href="/">返回首页</Link>
</div>
);
}
06 Navigation
For client-side navigation in App Router, use next/link and next/navigation (do not use the legacy useRouter from next/router for App Router page logic).
Link: Declarative navigation + prefetching
import Link from "next/link";
export default function Nav() {
return (
<nav className="flex gap-4">
<Link href="/about" prefetch>
关于
</Link>
<Link href="/posts/first" replace>
文章(替换历史记录)
</Link>
</nav>
);
}
useRouter: Programmatic navigation (client component)
"use client";
import { useRouter } from "next/navigation";
export function GoDashboardButton() {
const router = useRouter();
return (
<button type="button" onClick={() => router.push("/dashboard")}>
进入控制台
</button>
);
}
redirect: Server-side / Server Action redirect
import { redirect } from "next/navigation";
export default async function AdminPage() {
const role = "guest"; // 实际应从会话或数据库读取
if (role !== "admin") {
redirect("/login");
}
return <h1>管理后台</h1>;
}
usePathname & useSearchParams (client-side)
"use client";
import { usePathname, useSearchParams } from "next/navigation";
export function DebugRoute() {
const pathname = usePathname();
const searchParams = useSearchParams();
const tab = searchParams.get("tab");
return (
<p>
当前路径:{pathname};查询参数 tab:{tab ?? "无"}
</p>
);
}
Tip: Client subtrees containing useSearchParams may need an outer <Suspense> wrapper in static rendering scenarios to avoid build warnings. See the official docs on "Static Rendering & Dynamic APIs".
07 Chapter Summary
Route Entry
Use page.tsx to define accessible paths; directory structure maps to URLs.
Layout Persistence
layout.tsx nests and wraps child routes; template.tsx remounts on every navigation.
Dynamic Segments
[], [...], [[...]] cover single, multi-level, and optional multi-level segments; in Next.js 15 server-side, use await params.
UX & Boundaries
loading / error / not-found have clear responsibilities; route groups () organize code without changing URLs.
Navigation API
Link, useRouter, redirect, usePathname, useSearchParams cover declarative, imperative, and URL parsing needs.