All articles

Next.js App Router: The Complete Guide

The App Router is a paradigm shift in Next.js that brings powerful new features like layouts, nested routing, and enhanced server-side rendering capabilities. Let's dive deep into how to leverage these features effectively.

Understanding the App Directory Structure

The App Router uses a file-system based routing where folders define routes and special files define UI:

app/
├── layout.tsx          # Root layout
├── page.tsx           # Home page
├── loading.tsx        # Loading UI
├── error.tsx          # Error UI
├── not-found.tsx      # 404 page
└── dashboard/
    ├── layout.tsx     # Dashboard layout
    ├── page.tsx       # Dashboard page
    ├── settings/
    │   └── page.tsx   # Settings page
    └── analytics/
        └── page.tsx   # Analytics page

Creating Layouts

Layouts are shared UI that wrap multiple pages:

// app/layout.tsx
import { Inter } from 'next/font/google'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <nav className="bg-blue-600 text-white p-4">
          <h1>My Application</h1>
        </nav>
        <main className="container mx-auto p-4">
          {children}
        </main>
      </body>
    </html>
  )
}

Nested Layouts

Create section-specific layouts that only apply to certain routes:

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="flex">
      <aside className="w-64 bg-gray-100 min-h-screen p-4">
        <nav>
          <ul className="space-y-2">
            <li><a href="/dashboard">Overview</a></li>
            <li><a href="/dashboard/analytics">Analytics</a></li>
            <li><a href="/dashboard/settings">Settings</a></li>
          </ul>
        </nav>
      </aside>
      <div className="flex-1 p-8">
        {children}
      </div>
    </div>
  )
}

Dynamic Routes

Create dynamic routes using square brackets:

// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
  return (
    <article>
      <h1>Blog Post: {params.slug}</h1>
      {/* Fetch and display blog content */}
    </article>
  )
}

// Generate static params for static generation
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(res => res.json())

  return posts.map((post: any) => ({
    slug: post.slug,
  }))
}

Server and Client Components

By default, components in the App Router are Server Components:

// app/posts/page.tsx (Server Component)
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'no-store' // This will fetch fresh data on every request
  })
  return res.json()
}

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

  return (
    <div>
      <h1>Latest Posts</h1>
      {posts.map((post: any) => (
        <article key={post.id} className="mb-6">
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

For interactive components, use the 'use client' directive:

// components/search-form.tsx (Client Component)
'use client'

import { useState } from 'react'

export default function SearchForm() {
  const [query, setQuery] = useState('')

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    // Handle search logic
    console.log('Searching for:', query)
  }

  return (
    <form onSubmit={handleSubmit} className="mb-6">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search posts..."
        className="border p-2 rounded mr-2"
      />
      <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
        Search
      </button>
    </form>
  )
}

Loading and Error States

Create better user experiences with loading and error boundaries:

// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div className="flex justify-center items-center min-h-screen">
      <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900"></div>
    </div>
  )
}

// app/dashboard/error.tsx
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div className="text-center py-8">
      <h2 className="text-2xl font-bold text-red-600 mb-4">Something went wrong!</h2>
      <p className="text-gray-600 mb-4">{error.message}</p>
      <button
        onClick={reset}
        className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
      >
        Try again
      </button>
    </div>
  )
}

Data Fetching Patterns

The App Router provides several ways to fetch data:

// Static data fetching
async function getStaticData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'force-cache' // This is the default
  })
  return res.json()
}

// Dynamic data fetching
async function getDynamicData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'no-store'
  })
  return res.json()
}

// Revalidated data fetching
async function getRevalidatedData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 } // Revalidate every hour
  })
  return res.json()
}

Route Groups

Organize routes without affecting URL structure using parentheses:

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

Migration from Pages Router

To migrate from the Pages Router:

  1. Move pages from pages/ to app/ directory
  2. Convert to layouts instead of _app.tsx
  3. Update data fetching from getServerSideProps/getStaticProps to async components
  4. Add 'use client' directive to components that need client-side features

Best Practices

  1. Use Server Components by default - only add 'use client' when needed
  2. Colocate related files - keep components, styles, and tests near the routes that use them
  3. Leverage layouts for shared UI and avoid prop drilling
  4. Use loading.tsx for better perceived performance
  5. Implement proper error boundaries with error.tsx files
  6. Optimize data fetching using appropriate caching strategies

The App Router represents the future of Next.js development, providing better performance, developer experience, and more intuitive patterns for building modern web applications.

Next.js App Router: The Complete Guide