Skip to content

Next.js App Router

App Router is the new routing system introduced in Next.js 13, built on React Server Components, providing more powerful features and a better development experience. This chapter will detail the core concepts and usage of App Router.

App Router Overview

Main Features

  • Server Components - Rendered on the server by default for better performance
  • Nested Layouts - Support for complex layout structures
  • Streaming - Progressive page content loading
  • Parallel Routes - Render multiple page segments simultaneously
  • Intercepting Routes - Intercept and rewrite routes
  • Better Data Fetching - Simplified async components

Directory Structure

app/
├── layout.tsx          # Root layout
├── page.tsx           # Homepage
├── loading.tsx        # Loading UI
├── error.tsx          # Error UI
├── not-found.tsx      # 404 page
├── global-error.tsx   # Global error handling
├── blog/
│   ├── layout.tsx     # Blog layout
│   ├── page.tsx       # Blog homepage
│   ├── loading.tsx    # Blog loading UI
│   └── [slug]/
│       └── page.tsx   # Blog post page
└── dashboard/
    ├── layout.tsx     # Dashboard layout
    ├── page.tsx       # Dashboard homepage
    ├── analytics/
    │   └── page.tsx   # Analytics page
    └── settings/
        └── page.tsx   # Settings page

File Conventions

Special Files

FilenamePurposeRequired
layout.tsxLayout component
page.tsxPage component
loading.tsxLoading UI
error.tsxError UI
not-found.tsx404 UI
global-error.tsxGlobal error UI
route.tsxAPI route
template.tsxTemplate component

Dynamic Routes

app/
├── [id]/              # Dynamic route segment
├── [...slug]/         # Catch-all route
├── [[...slug]]/       # Optional catch-all route
└── (group)/           # Route group (doesn't affect URL)

Layout System

Root Layout

typescript
// app/layout.tsx
import './globals.css'

export const metadata = {
  title: 'My App',
  description: 'Generated by Next.js',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <header>
          <nav>Navigation</nav>
        </header>
        <main>{children}</main>
        <footer>Footer</footer>
      </body>
    </html>
  )
}

Nested Layouts

typescript
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="dashboard">
      <aside className="sidebar">
        <nav>
          <a href="/dashboard">Overview</a>
          <a href="/dashboard/analytics">Analytics</a>
          <a href="/dashboard/settings">Settings</a>
        </nav>
      </aside>
      <div className="content">
        {children}
      </div>
    </div>
  )
}

Layout Composition

typescript
// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="blog-layout">
      <div className="blog-header">
        <h1>My Blog</h1>
      </div>
      <div className="blog-content">
        {children}
      </div>
      <div className="blog-sidebar">
        <h3>Recent Posts</h3>
        {/* Sidebar content */}
      </div>
    </div>
  )
}

Server Components

Default Server Components

typescript
// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()
  
  return (
    <div>
      <h1>Posts List</h1>
      {posts.map((post: any) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

Parallel Data Fetching

typescript
// app/dashboard/page.tsx
async function getUser() {
  const res = await fetch('https://api.example.com/user')
  return res.json()
}

async function getStats() {
  const res = await fetch('https://api.example.com/stats')
  return res.json()
}

async function getNotifications() {
  const res = await fetch('https://api.example.com/notifications')
  return res.json()
}

export default async function Dashboard() {
  // Fetch data in parallel
  const [user, stats, notifications] = await Promise.all([
    getUser(),
    getStats(),
    getNotifications()
  ])
  
  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <div className="stats">
        <div>Visits: {stats.visits}</div>
        <div>Users: {stats.users}</div>
      </div>
      <div className="notifications">
        {notifications.map((notif: any) => (
          <div key={notif.id}>{notif.message}</div>
        ))}
      </div>
    </div>
  )
}

Client Components

Using 'use client' Directive

typescript
// app/components/Counter.tsx
'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  )
}

Mixing Server and Client Components

typescript
// app/blog/[slug]/page.tsx (Server Component)
import CommentForm from './CommentForm'
import LikeButton from './LikeButton'

async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`)
  return res.json()
}

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
      
      {/* Client Components */}
      <LikeButton postId={post.id} />
      <CommentForm postId={post.id} />
    </article>
  )
}
typescript
// app/blog/[slug]/LikeButton.tsx (Client Component)
'use client'

import { useState } from 'react'

export default function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false)
  const [likes, setLikes] = useState(0)
  
  const handleLike = async () => {
    const response = await fetch(`/api/posts/${postId}/like`, {
      method: 'POST'
    })
    const data = await response.json()
    setLiked(!liked)
    setLikes(data.likes)
  }
  
  return (
    <button
      onClick={handleLike}
      className={`like-btn ${liked ? 'liked' : ''}`}
    >
      ❤️ {likes}
    </button>
  )
}

Streaming and Suspense

Basic Suspense Usage

typescript
// app/dashboard/page.tsx
import { Suspense } from 'react'
import UserProfile from './UserProfile'
import RecentActivity from './RecentActivity'

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      
      <Suspense fallback={<div>Loading user info...</div>}>
        <UserProfile />
      </Suspense>
      
      <Suspense fallback={<div>Loading activity...</div>}>
        <RecentActivity />
      </Suspense>
    </div>
  )
}

Nested Suspense

typescript
// app/blog/page.tsx
import { Suspense } from 'react'

async function FeaturedPosts() {
  const posts = await fetchFeaturedPosts()
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h3>{post.title}</h3>
          <Suspense fallback={<div>Loading comments...</div>}>
            <Comments postId={post.id} />
          </Suspense>
        </article>
      ))}
    </div>
  )
}

async function Comments({ postId }: { postId: string }) {
  const comments = await fetchComments(postId)
  return (
    <div>
      {comments.map(comment => (
        <div key={comment.id}>{comment.text}</div>
      ))}
    </div>
  )
}

export default function BlogPage() {
  return (
    <div>
      <h1>Blog</h1>
      <Suspense fallback={<div>Loading featured posts...</div>}>
        <FeaturedPosts />
      </Suspense>
    </div>
  )
}

Loading and Error Handling

Loading UI

typescript
// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="loading">
      <div className="spinner"></div>
      <p>Loading blog content...</p>
    </div>
  )
}

Error Handling

typescript
// app/blog/error.tsx
'use client'

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

Global Error Handling

typescript
// app/global-error.tsx
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <div className="global-error">
          <h1>Application Error</h1>
          <p>An unexpected error occurred</p>
          <button onClick={() => reset()}>Try again</button>
        </div>
      </body>
    </html>
  )
}

Route Groups

Organizing Route Structure

app/
├── (marketing)/        # Marketing pages group
│   ├── about/
│   │   └── page.tsx
│   ├── contact/
│   │   └── page.tsx
│   └── layout.tsx      # Marketing layout
├── (shop)/            # Shop pages group
│   ├── products/
│   │   └── page.tsx
│   ├── cart/
│   │   └── page.tsx
│   └── layout.tsx      # Shop layout
└── layout.tsx          # Root layout
typescript
// app/(marketing)/layout.tsx
export default function MarketingLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="marketing-layout">
      <nav className="marketing-nav">
        <a href="/about">About Us</a>
        <a href="/contact">Contact Us</a>
      </nav>
      {children}
    </div>
  )
}

Parallel Routes

Using @folder Syntax

app/
├── dashboard/
│   ├── @analytics/     # Parallel route segment
│   │   └── page.tsx
│   ├── @team/          # Parallel route segment
│   │   └── page.tsx
│   ├── layout.tsx
│   └── page.tsx
typescript
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <div className="dashboard">
      <div className="main">{children}</div>
      <div className="analytics">{analytics}</div>
      <div className="team">{team}</div>
    </div>
  )
}

Intercepting Routes

Using (.) Syntax

app/
├── feed/
│   ├── (..)photo/      # Intercept /photo
│   │   └── [id]/
│   │       └── page.tsx
│   └── page.tsx
└── photo/
    └── [id]/
        └── page.tsx
typescript
// app/feed/(..)photo/[id]/page.tsx
import Modal from '@/components/Modal'

export default function PhotoModal({ params }: { params: { id: string } }) {
  return (
    <Modal>
      <img src={`/photos/${params.id}.jpg`} alt="Photo" />
    </Modal>
  )
}

Metadata API

Static Metadata

typescript
// app/blog/page.tsx
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My Blog',
  description: 'Sharing tech and life',
  keywords: ['blog', 'tech', 'Next.js'],
  openGraph: {
    title: 'My Blog',
    description: 'Sharing tech and life',
    images: ['/og-image.jpg'],
  },
}

export default function BlogPage() {
  return <div>Blog content</div>
}

Dynamic Metadata

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

interface Props {
  params: { slug: string }
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await fetchPost(params.slug)
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  }
}

export default async function BlogPost({ params }: Props) {
  const post = await fetchPost(params.slug)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Middleware Integration

Middleware Configuration

typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Check authentication
  const token = request.cookies.get('token')
  
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
  }
  
  // Add custom header
  const response = NextResponse.next()
  response.headers.set('X-Custom-Header', 'custom-value')
  
  return response
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*']
}

Performance Optimization

Prefetching Strategy

typescript
// app/components/PostLink.tsx
'use client'

import Link from 'next/link'
import { useRouter } from 'next/navigation'

export default function PostLink({ post }: { post: Post }) {
  const router = useRouter()
  
  return (
    <Link
      href={`/blog/${post.slug}`}
      onMouseEnter={() => {
        // Prefetch page
        router.prefetch(`/blog/${post.slug}`)
      }}
    >
      {post.title}
    </Link>
  )
}

Cache Configuration

typescript
// app/api/posts/route.ts
export const revalidate = 3600 // Revalidate every hour

export async function GET() {
  const posts = await fetchPosts()
  return Response.json(posts)
}

Migration Guide

Migrating from Pages Router

  1. Create app directory
  2. Move page files
  3. Update layout structure
  4. Convert data fetching methods
  5. Update API routes
typescript
// Old: pages/blog/[slug].tsx
export async function getStaticProps({ params }) {
  const post = await fetchPost(params.slug)
  return { props: { post } }
}

export default function BlogPost({ post }) {
  return <div>{post.title}</div>
}

// New: app/blog/[slug]/page.tsx
export default async function BlogPost({ params }) {
  const post = await fetchPost(params.slug)
  return <div>{post.title}</div>
}

Best Practices

1. Use Server and Client Components Wisely

typescript
// Server Component for data fetching
async function PostList() {
  const posts = await fetchPosts()
  return (
    <div>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  )
}

// Client Component for interactivity
'use client'
function PostCard({ post }) {
  const [liked, setLiked] = useState(false)
  
  return (
    <div>
      <h3>{post.title}</h3>
      <button onClick={() => setLiked(!liked)}>
        {liked ? '❤️' : '🤍'}
      </button>
    </div>
  )
}

2. Optimize Loading Experience

typescript
// Use Suspense for better loading experience
export default function Page() {
  return (
    <div>
      <h1>Page Title</h1>
      <Suspense fallback={<PostsSkeleton />}>
        <PostList />
      </Suspense>
    </div>
  )
}

3. Error Boundary Strategy

typescript
// Provide error handling for different levels
// app/error.tsx - App level errors
// app/blog/error.tsx - Blog module errors
// app/blog/[slug]/error.tsx - Post page errors

Summary

App Router brings many improvements:

  • Better performance - Server Components and streaming
  • More flexible layouts - Nested layouts and parallel routes
  • Simpler data fetching - Async components and parallel requests
  • Better user experience - Loading states and error handling
  • Stronger type safety - Full TypeScript support

App Router is the future direction of Next.js. It's recommended for new projects, and existing projects can migrate gradually.

Content is for learning and research only.