App Router File-System Routing
File system routing is like how books are categorized in a library—filenames determine the URL path, and the directory structure serves as your routing table, without the need for any manual configuration.
1. What You'll Learn
- The core principle of file system routing: The filename is the URL
- The Six File Conventions: page, layout, loading, error, not-found, route
- Dynamic Routing
[id]Single-Parameter Mode - Catch-all Routes
[...slug]and Optional Catch-all[[...catchAll]] - Defining and Using Routes with the Route Handlers API
2. A True Story of a Technical Manager
(1) Pain Point: Manual routing configuration is chaotic
Bob is the technical lead at an e-commerce company, and his team maintains a 50-page React SPA. Every time a new page is added, developers have to manually configure three files:
"Our routing configuration table
routes.jshas 300 lines. Last week, Xiao Ming added a new product detail page but forgot to registerpath: '/products/:id'in the routing table, resulting in a 404 error that went unnoticed for a full two hours after the page went live."
Bob's team encountered the following routing management issues:
| Issue | Impact | Frequency |
|---|---|---|
| Missing routing configuration | 404 after deployment | 1–2 times per month |
| Manual Handling of Nested Routes | Complex code, confusing route hierarchy | Each new page |
| Scattered 404/500 Error Pages | Inconsistent, poor user experience | Multiple pages |
| Separate Maintenance of API Routes | One set of routes for the front end and one for the back end | Each API |
(2) Solutions for Next.js File System Routing
The filename is the URL, and the directory structure is the routing table—there is no longer a need for
react-router-domrouting configuration.
src/app/
├── page.tsx # → /
├── about/
│ └── page.tsx # → /about
├── products/
│ ├── page.tsx # → /products
│ └── [id]/
│ ├── page.tsx # → /products/1, /products/2
│ └── reviews/
│ └── page.tsx # → /products/1/reviews
└── api/
└── products/
└── route.ts # → /api/products (GET/POST)
(3) Revenue
| Dimension | Before (React Router) | After (File System Routing) |
|---|---|---|
| Steps to Add a Page | 3 steps (create a component + configure routing + import) | 1 step (create a file) |
| Routing Configuration Maintenance | 300-line routing table | Zero lines, automatically determined by the directory |
| 404 Errors Upon Launch | 1–2 times per month | 0 times |
| Unified Management of 404/500 Errors | Manual import | File conventions take effect automatically |
3. Principles of File System Routing
(1) The filename is the URL
graph TB
subgraph "src/app/ Directory Structure"
A[page.tsx] --> B[/]
C[about/page.tsx] --> D[/about]
E[products/page.tsx] --> F[/products]
G[products/promo/page.tsx] --> H[/products/promo]
I[products/id/page.tsx] --> J[/products/:id]
K[dashboard/settings/page.tsx] --> L[/dashboard/settings]
end
style A fill:#d4edda
style C fill:#d4edda
style E fill:#d4edda
| File Name | Corresponding URL | Description |
|---|---|---|
app/page.tsx |
/ |
Home |
app/about/page.tsx |
/about |
Static Page |
app/blog/page.tsx |
/blog |
Blog List |
app/blog/[id]/page.tsx |
/blog/1 |
Dynamic Routing |
app/dashboard/settings/page.tsx |
/dashboard/settings |
Nested Multi-Level |
(2) Provisions in the Six Major Documents
| File Name | Purpose | Required? | Rendering Behavior |
|---|---|---|---|
page.tsx |
Page Component (UI Content) | ✅ | Server Component (default) |
layout.tsx |
Layout Container (Persistent State) | ✅ Root layout | Do not remount during navigation |
loading.tsx |
Load skeleton screen | Optional | Suspense fallback |
error.tsx |
Error Boundary UI | Optional | Capture Child Component Errors |
not-found.tsx |
404 Page | Optional | notFound() Trigger |
route.ts |
API Endpoint | Optional | Server-side Execution |
▶ Example: Creating Your First Page
Output:
Diagram: page.tsx; /; about/page.tsx; /about; products/page.tsx; /products.
// ============================================
// Create /about Page
// Document:src/app/about/page.tsx
// Just create a file,Automatic Route Registration
// ============================================
export default function AboutPage() {
return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold">About TaskFlow</h1>
<p className="mt-4 text-gray-600 leading-relaxed">
TaskFlow is a collaborative project management platform that helps
teams plan, track, and deliver projects efficiently. Built with
Next.js 16 and React 19, it provides real-time updates, seamless
collaboration, and enterprise-grade security.
</p>
<div className="mt-8 grid grid-cols-3 gap-4">
<div className="p-4 bg-blue-50 rounded-lg text-center">
<div className="text-2xl font-bold text-blue-600">10K+</div>
<div className="text-sm text-gray-500">Active Users</div>
</div>
<div className="p-4 bg-green-50 rounded-lg text-center">
<div className="text-2xl font-bold text-green-600">50K+</div>
<div className="text-sm text-gray-500">Projects</div>
</div>
<div className="p-4 bg-purple-50 rounded-lg text-center">
<div className="text-2xl font-bold text-purple-600">99.9%</div>
<div className="text-sm text-gray-500">Uptime</div>
</div>
</div>
</div>
);
}
Output:
Content: About TaskFlow | TaskFlow is a collaborative project | 10K+ | Active Users
Output:
Visit http://localhost:3000/about See:
About TaskFlow
TaskFlow is a collaborative project management platform...
10K+ Active Users | 50K+ Projects | 99.9% Uptime
4. Dynamic Routing
(1) Single-Parameter Dynamic Routing [id]
graph LR
A[app/products] --> B[page.tsx → /products]
A --> C[[id]]
C --> D[page.tsx → /products/1]
C --> E[/products/2]
style B fill:#d4edda
style D fill:#cce5ff
| File Mode | URL Example | params Value |
|---|---|---|
app/blog/[slug]/page.tsx |
/blog/hello-world |
{ slug: 'hello-world' } |
app/products/[id]/page.tsx |
/products/42 |
{ id: '42' } |
app/users/[userId]/settings/page.tsx |
/users/123/settings |
{ userId: '123' } |
▶ Example: Dynamic Product Details Page
Output:
Diagram: app/products; page.tsx → /products; [id; page.tsx → /products/1; /products/2.
// ============================================
// Product Details Page — Dynamic Routing [id]
// Document:src/app/products/[id]/page.tsx
// ============================================
// In Next.js 16, params is a Promise (asynchronous)
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await fetch(`https://fakestoreapi.com/products/${id}`);
return (
<div className="container mx-auto p-8">
<div className="flex gap-8">
<img
src={product.image}
alt={product.title}
className="w-80 h-80 object-contain"
/>
<div>
<h1 className="text-2xl font-bold">{product.title}</h1>
<p className="text-xl text-green-600 font-bold mt-2">
${product.price}
</p>
<p className="text-sm text-gray-500 mt-1">
Category: {product.category}
</p>
<p className="mt-4 text-gray-700">{product.description}</p>
</div>
</div>
</div>
);
}
Output:
Server component fetches and renders data.
Output:
Visit /products/1 See:
Fjallraven - Foldsack No. 1 Backpack
$109.95
Category: men's clothing
Your perfect pack for day trips and hikes...
(2) Catch-all Routes [...slug]
graph TB
A[app/docs] --> B[[...slug]]
B --> C[page.tsx]
C --> D[/docs/getting-started]
C --> E[/docs/guides/installation]
C --> F[/docs/api/authentication/overview]
style C fill:#cce5ff
| File Mode | URL | params.slug |
|---|---|---|
app/docs/[...slug]/page.tsx |
/docs |
404 (Level 1 is required) |
app/docs/[...slug]/page.tsx |
/docs/getting-started |
['getting-started'] |
app/docs/[...slug]/page.tsx |
/docs/guides/installation |
['guides', 'installation'] |
app/docs/[...slug]/page.tsx |
/docs/a/b/c |
['a', 'b', 'c'] |
▶ Example: Catch-all Documentation Page
Output:
Diagram: app/docs; [...slug; page.tsx; /docs/getting-started; /docs/guides/installation; /docs/api/authentication/overview.
// ============================================
// Catch-all Routing — Multi-level Document Pages
// Document:src/app/docs/[...slug]/page.tsx
// ============================================
export default async function DocsPage({
params,
}: {
params: Promise<{ slug: string[] }>;
}) {
const { slug } = await params;
return (
<div className="container mx-auto p-8">
<nav className="text-sm text-gray-500 mb-4">
Home / Docs / {slug.join(" / ")}
</nav>
<h1 className="text-3xl font-bold">Docs: {slug.join(" > ")}</h1>
<div className="mt-8 p-6 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="font-medium">You are viewing documentation for:</p>
<ul className="mt-2 list-disc list-inside">
{slug.map((segment, index) => (
<li key={index}>
Level {index + 1}: <code className="bg-gray-100 px-2 py-0.5 rounded">{segment}</code>
</li>
))}
</ul>
</div>
<p className="mt-4 text-gray-600">
Total depth: {slug.length} levels
</p>
</div>
);
}
Output:
DocsPage renders its UI.
Output:
Visit /docs/getting-started/installation See:
Home / Docs / getting-started / installation
Docs: getting-started > installation
You are viewing documentation for:
- Level 1: getting-started
- Level 2: installation
Total depth: 2 levels
(3) Optional Catch-all [[...catchAll]]
graph TB
A[app/categories] --> B[[[...catchAll]]]
B --> C[page.tsx]
C --> D[/categories]
C --> E[/categories/electronics]
C --> F[/categories/electronics/phones]
style C fill:#d4edda
style D fill:#d4edda
| File Mode | URL | params.catchAll |
|---|---|---|
app/categories/[[...catchAll]]/page.tsx |
/categories |
undefined |
app/categories/[[...catchAll]]/page.tsx |
/categories/electronics |
['electronics'] |
app/categories/[[...catchAll]]/page.tsx |
/categories/electronics/phones |
['electronics', 'phones'] |
▶ Example: Optional Catch-all Category Page
Output:
Diagram: app/categories; [[...catchAll; page.tsx; /categories; /categories/electronics; /categories/electronics/phones.
// ============================================
// Optional Catch-all — Category Browse Page
// Document:src/app/categories/[[...categories]]/page.tsx
// Both /categories and /categories/electronics take effect
// ============================================
export default async function CategoriesPage({
params,
}: {
params: Promise<{ categories?: string[] }>;
}) {
const { categories } = await params;
const allProducts = await fetch("https://fakestoreapi.com/products");
const categoriesList = [...new Set(allProducts.map(p => p.category))];
const filteredProducts = categories
? allProducts.filter(p => p.category === categories[0])
: allProducts;
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">
{categories ? `Category: ${categories[0]}` : "All Categories"}
</h1>
{!categories && (
<div className="flex gap-2 mb-8 flex-wrap">
{categoriesList.map(cat => (
<a
key={cat}
href={`/categories/${encodeURIComponent(cat)}`}
className="px-4 py-2 bg-gray-100 rounded-full hover:bg-blue-100"
>
{cat}
</a>
))}
</div>
)}
<div className="grid grid-cols-4 gap-6">
{filteredProducts.slice(0, 8).map(p => (
<div key={p.id} className="border rounded-lg p-4 hover:shadow-lg">
<img src={p.image} alt={p.title} className="h-40 mx-auto" />
<p className="mt-2 font-medium text-sm truncate">{p.title}</p>
<p className="text-green-600 font-bold">${p.price}</p>
</div>
))}
</div>
</div>
);
}
Output:
Server-side data fetch renders list of items.
Output:
Visit /categories See:
All Category Buttons(electronics, jewelery, men's clothing, women's clothing)
Visit /categories/electronics See:
Category: electronics
Show only electronics(8Product Card)
5. loading.tsx and error.tsx
(1) loading.tsx — Load the skeleton screen
graph LR
A[User Navigation] --> B[loading.tsx<br/>Wireframe Display]
B --> C[page.tsx<br/>Data Ready]
C --> D[Full Page]
style B fill:#f8d7da
style C fill:#d4edda
▶ Example: Loading a skeleton screen
Output:
Diagram: User Navigation; loading.tsx Wireframe Display; page.tsx Data Ready; Full Page.
// ============================================
// Loading the skeleton screen — Displayed when page data is retrieved
// Document:src/app/products/loading.tsx
// ============================================
export default function ProductsLoading() {
return (
<div className="container mx-auto p-8">
<div className="h-8 w-48 bg-gray-200 rounded animate-pulse mb-6" />
<div className="grid grid-cols-4 gap-6">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="border rounded-lg p-4">
<div className="h-40 bg-gray-200 rounded animate-pulse" />
<div className="h-4 bg-gray-200 rounded mt-2 animate-pulse" />
<div className="h-4 w-16 bg-gray-200 rounded mt-2 animate-pulse" />
</div>
))}
</div>
</div>
);
}
Output:
Renders a list of items using .map().
Output:
When visiting /products, display until data loading is complete:
8 A gray placeholder card(Animation with Pulses)
After the data has finished loading,The placeholder content is automatically replaced with the actual content
Flicker-free、No white screen,Smooth Transition
(2) error.tsx — Error Boundary
▶ Example: Error Page
Output:
The page renders as described above, with the UI updating based on the described behavior.
// ============================================
// Error Boundary — Catching Child Component Errors
// Document:src/app/products/error.tsx
// ============================================
'use client';
export default function ProductsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="container mx-auto p-8 text-center">
<div className="max-w-md mx-auto">
<h2 className="text-2xl font-bold text-red-600 mb-4">
Something went wrong!
</h2>
<p className="text-gray-600 mb-6">
{error.message || "Failed to load products. Please try again."}
</p>
<button
onClick={reset}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Try Again
</button>
</div>
</div>
);
}
Output:
Content: Something went wrong! | Try Again
Output:
When data retrieval from the product page fails,Show:
Something went wrong!
Failed to load products. Please try again.
[Try Again] button → Click to retry automatically
(3) not-found.tsx — 404 Page
6. Route Handlers (API Routes)
(1) Basic Usage of route.ts
graph LR
A[Client Request] --> B[route.ts]
B --> C[GET /api/products]
B --> D[POST /api/products]
B --> E[PUT /api/products/:id]
B --> F[DELETE /api/products/:id]
style B fill:#cce5ff
| HTTP Method | Exported Function | Purpose |
|---|---|---|
GET |
export async function GET() |
Read Data |
POST |
export async function POST() |
Create Data |
PUT |
export async function PUT() |
Update Data |
DELETE |
export async function DELETE() |
Delete Data |
PATCH |
export async function PATCH() |
Partial Update |
7. Complete Example: E-commerce Product Browsing System
// ============================================
// Comprehensive Example:E-commerce Product Browsing System
// Pages Covered、Layout、Loading、Error、API Routing
// ============================================
// src/app/layout.tsx — Root Layout
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="bg-gray-50">
<nav className="bg-white shadow-sm p-4">
<a href="/" className="text-xl font-bold text-blue-600">
ShopHub
</a>
</nav>
<main>{children}</main>
</body>
</html>
);
}
// src/app/page.tsx — Home
export default function HomePage() {
return (
<div className="container mx-auto p-8 text-center">
<h1 className="text-4xl font-bold">Welcome to ShopHub</h1>
<p className="mt-4 text-gray-600">
Browse our collection of amazing products.
</p>
<a
href="/products"
className="inline-block mt-6 px-8 py-3 bg-blue-600 text-white rounded-lg"
>
Browse Products
</a>
</div>
);
}
// src/app/products/page.tsx — Product List
export default async function ProductsPage() {
const products = await fetch(
"https://fakestoreapi.com/products"
);
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">All Products</h1>
<div className="grid grid-cols-4 gap-6">
{products.map(p => (
<a
key={p.id}
href={`/products/${p.id}`}
className="border rounded-lg p-4 bg-white hover:shadow-lg"
>
<img
src={p.image}
alt={p.title}
className="h-40 mx-auto"
/>
<p className="mt-2 font-medium text-sm">{p.title}</p>
<p className="text-green-600 font-bold mt-1">${p.price}</p>
</a>
))}
</div>
</div>
);
}
// src/app/products/[id]/page.tsx — Product Details
export default async function ProductDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await fetch(
`https://fakestoreapi.com/products/${id}`
);
if (!product) {
notFound();
}
return (
<div className="container mx-auto p-8">
<a
href="/products"
className="text-blue-600 hover:underline mb-4 inline-block"
>
← Back to products
</a>
<div className="flex gap-8 bg-white p-8 rounded-lg shadow">
<img
src={product.image}
alt={product.title}
className="w-96 h-96 object-contain"
/>
<div>
<h1 className="text-3xl font-bold">{product.title}</h1>
<p className="text-2xl text-green-600 font-bold mt-4">
${product.price}
</p>
<p className="mt-6 text-gray-700 leading-relaxed">
{product.description}
</p>
<button className="mt-6 px-8 py-3 bg-blue-600 text-white rounded-lg">
Add to Cart
</button>
</div>
</div>
</div>
);
}
// src/app/products/loading.tsx — Loading the skeleton screen
export default function ProductsLoading() {
return (
<div className="container mx-auto p-8">
<div className="h-8 w-48 bg-gray-200 rounded animate-pulse mb-6" />
<div className="grid grid-cols-4 gap-6">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="border rounded-lg p-4 bg-white">
<div className="h-40 bg-gray-200 rounded animate-pulse" />
<div className="h-4 bg-gray-200 rounded mt-2 animate-pulse" />
<div className="h-4 w-16 bg-gray-200 rounded mt-2 animate-pulse" />
</div>
))}
</div>
</div>
);
}
// src/app/products/error.tsx — Error Boundary
'use client';
export default function ProductsError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="container mx-auto p-8 text-center">
<h2 className="text-2xl font-bold text-red-600">
Failed to load products
</h2>
<button
onClick={reset}
className="mt-4 px-6 py-2 bg-blue-600 text-white rounded-lg"
>
Try Again
</button>
</div>
);
}
Expected Output:
Home(/):
Welcome to ShopHub → [Browse Products]
Product List(/products):
Loading... → 8 A gray skeleton screen
Loading complete → Product Card Grid(Includes images、Title、Price)
Product Details(/products/1):
Product Images (left) + Title/Price/Description/Add to Cart (right)
← Back to products Link Back to List
Error Status:
Failed to load products
[Try Again] button
404 Status:
Automatically from the root not-found.tsx Processing
❓ FAQ
params Promises in Next.js 16?params, searchParams, and similar variables were changed to Promises to support asynchronous page generation. If you use them directly params.id, you’ll encounter a TypeScript error; you must await params to retrieve their values.loading.tsx and Suspense?loading.tsx is a page-level Suspense fallback. Next.js automatically wraps loading.tsx in <Suspense>. If you need a component-level loading state, you should manually use <Suspense fallback={...}>.error.tsx need use client?error.tsx must be a Client Component because it needs to handle interactions (such as the reset button). Server Components cannot contain event handlers or hooks, so the 'use client' directive is required.route.ts and page.tsx coexist?page.tsx and route.ts are mutually exclusive. A directory can only contain one type of route handler—either a page (page.tsx) or an API (route.ts).📖 Summary
- File system routing: The filename serves as the URL path, eliminating the need for manual configuration of routing tables
page.tsxdefines the page content;layout.tsxdefines the layout container- Dynamic routing:
[id]matches a single parameter,[...slug]matches a multi-level path [[...catchAll]]Allows zero arguments (optional);[...slug]requires at least one argumentloading.tsxAutomatically wrap withSuspenseto display a loading skeleton screenerror.tsxis a client component that provides an error boundary and a retry buttonnot-found.tsxHandles 404 pages; thenotFound()function is triggered manuallyroute.tsDefines API endpoints that support GET/POST/PUT/DELETE
📝 Exercises
-
Basic Exercise (⭐): Create a page named
app/blog/[slug]/page.tsxin the project so that when/blog/hello-worldis accessed, the console outputsslug: hello-world. -
Advanced Exercise (⭐⭐): Create
app/api/todos/route.tsto implement a simple TODO API (GET returns a list, POST creates a TODO), and verify it by accessing it in a browser. -
Challenge (⭐⭐⭐): Create two implementations,
app/docs/[...slug]/page.tsxandapp/docs/[[...slug]]/page.tsx, to test the access behavior of/docs, and use a table to compare the differences between the two.



