What is App Router?

The App Router is a new paradigm in Next.js 13+ that uses React Server Components and provides advanced routing features.

Key Benefits: Server Components by default, streaming SSR, nested layouts, parallel routes, and improved data fetching.

App vs Pages Router

Feature App Router (New) Pages Router (Legacy)
Directory app/ pages/
Server Components ✅ Default ❌ Not available
Nested Layouts ✅ Built-in ❌ Manual
Streaming ✅ Yes ❌ No
Data Fetching async/await in components getServerSideProps, etc.

File-based Routing

Routes are defined by the folder structure in the app/ directory.

Basic Routes

app/
├── page.tsx              → /
├── about/
│   └── page.tsx         → /about
├── blog/
│   └── page.tsx         → /blog
└── contact/
    └── page.tsx         → /contact

Dynamic Routes

app/
├── blog/
│   ├── page.tsx             → /blog
│   └── [slug]/
│       └── page.tsx         → /blog/:slug

// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
  return <h1>Post: {params.slug}</h1>
}

Catch-all Routes

app/
└── docs/
    └── [...slug]/
        └── page.tsx         → /docs/a, /docs/a/b, /docs/a/b/c

// app/docs/[...slug]/page.tsx
export default function Docs({ params }: { params: { slug: string[] } }) {
  return <h1>Docs: {params.slug.join('/')}</h1>
}

Route Groups

Organize routes without affecting URL structure using (folder):

app/
├── (marketing)/
│   ├── about/
│   │   └── page.tsx     → /about
│   └── blog/
│       └── page.tsx     → /blog
└── (shop)/
    ├── products/
    │   └── page.tsx     → /products
    └── cart/
        └── page.tsx     → /cart

Private Folders

Prefix with underscore to exclude from routing:

app/
├── _components/         → Not a route
│   └── Button.tsx
└── dashboard/
    └── page.tsx         → /dashboard

Layouts

Layouts are UI that is shared between routes. They preserve state and don't re-render.

Root Layout (Required)

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <header>Global Header</header>
        {children}
        <footer>Global Footer</footer>
      </body>
    </html>
  )
}

Nested Layouts

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div>
      <aside>Dashboard Sidebar</aside>
      <main>{children}</main>
    </div>
  )
}

// app/dashboard/page.tsx will be wrapped by both layouts

Layout Hierarchy

app/
├── layout.tsx                  (Root Layout)
├── page.tsx                    (Home Page)
└── dashboard/
    ├── layout.tsx              (Dashboard Layout)
    ├── page.tsx                (Dashboard Page)
    └── settings/
        └── page.tsx            (Settings Page)

// Rendering hierarchy for /dashboard/settings:
// RootLayout → DashboardLayout → SettingsPage
Performance: Layouts don't re-render on navigation, improving performance and preserving state.

Pages

A page is UI that is unique to a route. Define by exporting a component from a page.tsx file.

Basic Page

// app/about/page.tsx
export default function AboutPage() {
  return <h1>About Us</h1>
}

Page with Params

// app/blog/[slug]/page.tsx
export default function BlogPost({
  params,
  searchParams,
}: {
  params: { slug: string }
  searchParams: { [key: string]: string | string[] | undefined }
}) {
  return (
    <div>
      <h1>Post: {params.slug}</h1>
      <p>Sort: {searchParams.sort}</p>
    </div>
  )
}

// URL: /blog/hello-world?sort=asc
// params.slug = "hello-world"
// searchParams.sort = "asc"

Metadata (SEO)

// app/blog/[slug]/page.tsx
import { Metadata } from 'next'

export async function generateMetadata({
  params,
}: {
  params: { slug: string }
}): Promise<Metadata> {
  return {
    title: `Blog Post: ${params.slug}`,
    description: 'Blog post description',
  }
}

export default function BlogPost({ params }: { params: { slug: string } }) {
  return <h1>{params.slug}</h1>
}

Loading UI & Streaming

Create instant loading states with loading.tsx and stream content as it becomes ready.

Loading File

// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div>
      <h2>Loading...</h2>
      <div className="spinner"></div>
    </div>
  )
}

Streaming with Suspense

import { Suspense } from 'react'

async function SlowComponent() {
  // Slow data fetch
  await new Promise(resolve => setTimeout(resolve, 3000))
  return <div>Loaded!</div>
}

export default function Page() {
  return (
    <div>
      <h1>Page Content</h1>

      <Suspense fallback={<div>Loading slow component...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}
Streaming: Content loads progressively, showing loading states for slower parts while fast content appears instantly.

Error Handling

Handle errors gracefully with error.tsx and not-found.tsx files.

Error Boundary

// app/dashboard/error.tsx
'use client' // Error components must be Client Components

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}

Not Found

// app/blog/[slug]/not-found.tsx
export default function NotFound() {
  return (
    <div>
      <h2>404 - Post Not Found</h2>
      <p>Could not find the requested blog post.</p>
    </div>
  )
}

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)

  if (!post) {
    notFound() // Triggers not-found.tsx
  }

  return <h1>{post.title}</h1>
}

Global Not Found

// app/not-found.tsx
export default function GlobalNotFound() {
  return (
    <div>
      <h1>404 - Page Not Found</h1>
      <p>The page you're looking for doesn't exist.</p>
    </div>
  )
}

Data Fetching

Fetch data directly in Server Components using async/await.

Server Component Data Fetching

// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'no-store', // Disable caching (dynamic)
  })
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Caching Strategies

// Static (cached by default)
fetch('https://api.example.com/posts')

// Revalidate every 60 seconds
fetch('https://api.example.com/posts', {
  next: { revalidate: 60 }
})

// Dynamic (always fresh)
fetch('https://api.example.com/posts', {
  cache: 'no-store'
})

// Force cache
fetch('https://api.example.com/posts', {
  cache: 'force-cache'
})

Parallel Data Fetching

async function Page() {
  // Fetch in parallel
  const [user, posts] = await Promise.all([
    fetch('https://api.example.com/user').then(r => r.json()),
    fetch('https://api.example.com/posts').then(r => r.json()),
  ])

  return (
    <div>
      <h1>{user.name}</h1>
      <PostList posts={posts} />
    </div>
  )
}
Important: Data fetching in Server Components happens on the server, keeping API keys and sensitive data secure.

Server Actions

Server Actions enable you to run server-side code directly from Client Components without API routes.

Basic Server Action

// app/actions.ts
'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title')
  const content = formData.get('content')

  // Save to database
  await db.post.create({
    data: { title, content }
  })

  revalidatePath('/posts')
}

// app/create/page.tsx
import { createPost } from '../actions'

export default function CreatePost() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required /></textarea>
      <button type="submit">Create Post</button>
    </form>
  )
}

With Client Component

// app/actions.ts
'use server'

export async function incrementLikes(postId: string) {
  await db.post.update({
    where: { id: postId },
    data: { likes: { increment: 1 } }
  })
  revalidatePath('/posts')
  return { success: true }
}

// app/components/LikeButton.tsx
'use client'

import { incrementLikes } from '../actions'
import { useState } from 'react'

export default function LikeButton({ postId }: { postId: string }) {
  const [isPending, setIsPending] = useState(false)

  async function handleLike() {
    setIsPending(true)
    await incrementLikes(postId)
    setIsPending(false)
  }

  return (
    <button onClick={handleLike} disabled={isPending}>
      {isPending ? 'Liking...' : 'Like'}
    </button>
  )
}

Server Action Benefits

  • No API Routes: Write backend logic without creating endpoints
  • Type Safety: Full TypeScript support end-to-end
  • Progressive Enhancement: Forms work without JavaScript
  • Security: Server code never sent to client
  • Revalidation: Automatically update cached data
Best Practice: Use Server Actions for mutations (create, update, delete) to keep your app secure and performant.