- Learn
- Stack Essentials
- Next.js
- App Router & File Structure
Master Next.js App Router conventions for file-based routing, layouts, and special files that AI tools rely on.
App Router & File Structure
The App Router is Next.js's file-based routing system. Understanding its conventions helps you navigate and modify AI-generated code effectively.
Directory Structure
Standard Next.js Project
my-app/
├── app/ # App Router directory
│ ├── layout.tsx # Root layout (required)
│ ├── page.tsx # Home page (/)
│ ├── globals.css # Global styles
│ ├── favicon.ico # Site favicon
│ ├── about/
│ │ └── page.tsx # About page (/about)
│ └── api/
│ └── users/
│ └── route.ts # API endpoint (/api/users)
├── components/ # Reusable components
├── lib/ # Utility functions
├── public/ # Static files
├── package.json
├── next.config.js # Next.js configuration
├── tailwind.config.ts # Tailwind configuration
└── tsconfig.json # TypeScript configuration
AI-Generated Structure
AI tools often add more organization:
src/
├── app/
│ ├── (auth)/ # Route group for auth pages
│ │ ├── login/
│ │ └── signup/
│ ├── (dashboard)/ # Route group for dashboard
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── settings/
│ └── api/
├── components/
│ ├── ui/ # Base UI components
│ ├── forms/ # Form components
│ └── layouts/ # Layout components
├── lib/
│ ├── utils.ts
│ ├── db.ts
│ └── auth.ts
├── hooks/ # Custom React hooks
├── types/ # TypeScript types
└── styles/ # Additional styles
Special Files
page.tsx
Makes a route publicly accessible:
// app/products/page.tsx → /products
export default function ProductsPage() {
return (
<main>
<h1>Products</h1>
<ProductList />
</main>
)
}
Rules:
- Must export a default component
- Can be async (Server Component)
- Receives
paramsandsearchParamsprops (as Promises in Next.js 15+)
layout.tsx
Wraps pages with shared UI:
// app/layout.tsx - Root layout
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
)
}
// app/dashboard/layout.tsx - Nested layout
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex">
<Sidebar />
<div className="flex-1">{children}</div>
</div>
)
}
Rules:
- Root layout must have
<html>and<body> - Nested layouts wrap their segment's pages
- Layouts don't re-render when navigating between child pages
loading.tsx
Shows while the page loads:
// app/products/loading.tsx
export default function Loading() {
return (
<div className="flex justify-center p-8">
<Spinner />
</div>
)
}
Auto-wrapped in Suspense:
// Next.js automatically does this:
<Suspense fallback={<Loading />}>
<Page />
</Suspense>
error.tsx
Handles errors in a segment:
// app/products/error.tsx
'use client' // Must be a client component
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div className="p-8">
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
not-found.tsx
Custom 404 page:
// app/not-found.tsx - Global 404
export default function NotFound() {
return (
<div className="text-center py-20">
<h1 className="text-4xl font-bold">404</h1>
<p>Page not found</p>
<Link href="/">Go home</Link>
</div>
)
}
// app/products/[id]/not-found.tsx - Segment-specific
import { notFound } from 'next/navigation'
// In page.tsx, call notFound() when needed:
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const product = await getProduct(id)
if (!product) {
notFound() // Renders not-found.tsx
}
return <ProductDetails product={product} />
}
route.ts
API route handlers:
// app/api/products/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const products = await db.product.findMany()
return NextResponse.json(products)
}
export async function POST(request: Request) {
const data = await request.json()
const product = await db.product.create({ data })
return NextResponse.json(product, { status: 201 })
}
Dynamic Routes
Single Parameter
// app/products/[id]/page.tsx → /products/123
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return <h1>Product {id}</h1>
}
Next.js 15+ Change:
paramsis now a Promise and must be awaited.
Multiple Parameters
// app/shop/[category]/[product]/page.tsx
// → /shop/electronics/laptop
export default async function ProductPage({
params,
}: {
params: Promise<{ category: string; product: string }>
}) {
const { category, product } = await params
return <h1>{product} in {category}</h1>
}
Catch-All Routes
// app/docs/[...slug]/page.tsx
// → /docs/getting-started
// → /docs/api/authentication/oauth
export default async function DocsPage({
params,
}: {
params: Promise<{ slug: string[] }>
}) {
const { slug } = await params
// slug = ['api', 'authentication', 'oauth']
return <h1>Docs: {slug.join('/')}</h1>
}
Optional Catch-All
// app/shop/[[...slug]]/page.tsx
// → /shop (slug = undefined)
// → /shop/electronics (slug = ['electronics'])
// → /shop/electronics/phones (slug = ['electronics', 'phones'])
Route Groups
Organize without affecting URL:
app/
├── (marketing)/ # Group - not in URL
│ ├── layout.tsx # Marketing layout
│ ├── page.tsx # → /
│ ├── about/
│ │ └── page.tsx # → /about
│ └── pricing/
│ └── page.tsx # → /pricing
└── (dashboard)/ # Group - not in URL
├── layout.tsx # Dashboard layout
├── dashboard/
│ └── page.tsx # → /dashboard
└── settings/
└── page.tsx # → /settings
Use cases:
- Different layouts for different sections
- Organizing related routes
- Splitting the root layout
Parallel Routes
Multiple pages in one layout:
app/
├── layout.tsx
├── page.tsx
├── @modal/ # Parallel route slot
│ └── (.)products/[id]/ # Intercepted route
│ └── page.tsx
└── products/
└── [id]/
└── page.tsx
// app/layout.tsx
export default function Layout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<>
{children}
{modal}
</>
)
}
Intercepting Routes
Show modals while preserving URL:
(.) - Same level
(..) - One level up
(..)(..) - Two levels up
(...) - Root level
app/
├── products/
│ └── [id]/
│ └── page.tsx # Full page view
└── @modal/
└── (.)products/[id]/ # Intercepts /products/[id]
└── page.tsx # Shows in modal
Private Folders
Exclude from routing with underscore:
app/
├── _components/ # Not a route
│ ├── Header.tsx
│ └── Footer.tsx
├── _lib/ # Not a route
│ └── utils.ts
└── page.tsx
Metadata
Static Metadata
// app/about/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About Us',
description: 'Learn about our company',
}
export default function AboutPage() {
return <h1>About</h1>
}
Dynamic Metadata
// app/products/[id]/page.tsx
import { Metadata } from 'next'
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>
}): Promise<Metadata> {
const { id } = await params
const product = await getProduct(id)
return {
title: product.name,
description: product.description,
openGraph: {
images: [product.image],
},
}
}
Working with AI-Generated Routes
Common Patterns
CRUD Resource:
app/products/
├── page.tsx # List all (/products)
├── new/
│ └── page.tsx # Create form (/products/new)
└── [id]/
├── page.tsx # View one (/products/123)
└── edit/
└── page.tsx # Edit form (/products/123/edit)
Dashboard:
app/(dashboard)/
├── layout.tsx # Sidebar + header
├── page.tsx # Overview
├── analytics/
│ └── page.tsx
├── users/
│ └── page.tsx
└── settings/
└── page.tsx
Asking AI for Routes
"Add a blog section with:
- List page at /blog
- Individual posts at /blog/[slug]
- Category filtering at /blog/category/[category]"
"Create an admin layout with:
- Shared sidebar navigation
- Separate from the main site layout
- Pages for users, products, and orders"
Summary
- page.tsx creates routes
- layout.tsx wraps pages with shared UI
- loading.tsx and error.tsx handle states
- [param] creates dynamic routes
- (group) organizes without URL impact
- route.ts handles API endpoints
- _folder excludes from routing
Next Steps
Learn how Server and Client Components work together in the next lesson.