Beginner15 min1 prerequisite

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

Terminal
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:

Terminal
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:

Terminal
// 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 params and searchParams props (as Promises in Next.js 15+)

layout.tsx

Wraps pages with shared UI:

Terminal
// 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>
  )
}
Terminal
// 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:

Terminal
// app/products/loading.tsx
export default function Loading() {
  return (
    <div className="flex justify-center p-8">
      <Spinner />
    </div>
  )
}

Auto-wrapped in Suspense:

Terminal
// Next.js automatically does this:
<Suspense fallback={<Loading />}>
  <Page />
</Suspense>

error.tsx

Handles errors in a segment:

Terminal
// 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:

Terminal
// 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>
  )
}
Terminal
// 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:

Terminal
// 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

Terminal
// 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: params is now a Promise and must be awaited.

Multiple Parameters

Terminal
// 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

Terminal
// 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

Terminal
// app/shop/[[...slug]]/page.tsx
//  /shop (slug = undefined)
//  /shop/electronics (slug = ['electronics'])
//  /shop/electronics/phones (slug = ['electronics', 'phones'])

Route Groups

Organize without affecting URL:

Terminal
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:

Terminal
app/
├── layout.tsx
├── page.tsx
├── @modal/               # Parallel route slot
   └── (.)products/[id]/ # Intercepted route
       └── page.tsx
└── products/
    └── [id]/
        └── page.tsx
Terminal
// 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:

Terminal
(.)     - Same level
(..)    - One level up
(..)(..) - Two levels up
(...)   - Root level
Terminal
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:

Terminal
app/
├── _components/           # Not a route
   ├── Header.tsx
   └── Footer.tsx
├── _lib/                  # Not a route
   └── utils.ts
└── page.tsx

Metadata

Static Metadata

Terminal
// 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

Terminal
// 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:

Terminal
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:

Terminal
app/(dashboard)/
├── layout.tsx            # Sidebar + header
├── page.tsx              # Overview
├── analytics/
   └── page.tsx
├── users/
   └── page.tsx
└── settings/
    └── page.tsx

Asking AI for Routes

Terminal
"Add a blog section with:
- List page at /blog
- Individual posts at /blog/[slug]
- Category filtering at /blog/category/[category]"
Terminal
"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.

Mark this lesson as complete to track your progress