Developer Guide

Next.js App Router Complete Guide: From Basics to Advanced Patterns

Master the Next.js App Router with this comprehensive guide covering routing, layouts, server components, data fetching, and advanced patterns for building modern web applications.

2026-02-19

Next.js App Router Complete Guide: From Basics to Advanced Patterns#

The Next.js App Router represents a fundamental shift in how we build React applications. This comprehensive guide will take you from the basics to advanced patterns, helping you master modern Next.js development.

Table of Contents#

  1. Understanding the App Router
  2. File-Based Routing
  3. Layouts and Templates
  4. Server and Client Components
  5. Data Fetching Strategies
  6. Route Handlers (API Routes)
  7. Advanced Patterns

Understanding the App Router#

The App Router is built on React Server Components and introduces new conventions for building applications with improved performance and developer experience.

Key Benefits#

  • Server-First Architecture: Components render on the server by default
  • Streaming and Suspense: Progressive rendering for better UX
  • Nested Layouts: Share UI across routes efficiently
  • Colocation: Keep related files together
  • Improved Data Fetching: Fetch data where you need it

File-Based Routing#

Basic Route Structure#

app/
├── page.js                 # / route
├── about/
│   └── page.js            # /about route
├── blog/
│   ├── page.js            # /blog route
│   └── [slug]/
│       └── page.js        # /blog/[slug] dynamic route
└── dashboard/
    ├── layout.js          # Dashboard layout
    ├── page.js            # /dashboard route
    └── settings/
        └── page.js        # /dashboard/settings route

Dynamic Routes#

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

// Generate static params at build time
export async function generateStaticParams() {
  const posts = await getPosts()
  return posts.map((post) => ({
    slug: post.slug,
  }))
}

Catch-All Routes#

// app/docs/[...slug]/page.js
export default function Docs({ params }) {
  // params.slug will be an array: ['getting-started', 'installation']
  return <div>Docs: {params.slug.join('/')}</div>
}

Layouts and Templates#

Root Layout (Required)#

// app/layout.js
export const metadata = {
  title: 'My App',
  description: 'Welcome to my app',
}

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

Nested Layouts#

// app/dashboard/layout.js
export default function DashboardLayout({ children }) {
  return (
    <div className="dashboard">
      <aside>
        <DashboardNav />
      </aside>
      <section>{children}</section>
    </div>
  )
}

Templates (Re-render on Navigation)#

// app/template.js
export default function Template({ children }) {
  return <div className="animate-fade-in">{children}</div>
}

Server and Client Components#

Server Components (Default)#

// app/posts/page.js - Server Component
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'no-store', // Dynamic data
  })
  return res.json()
}

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

Client Components#

'use client'

import { useState } from 'react'

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

Composition Pattern#

// app/page.js - Server Component
import ClientComponent from './ClientComponent'

async function getData() {
  const data = await fetch('...')
  return data.json()
}

export default async function Page() {
  const data = await getData()
  
  return (
    <div>
      <h1>Server-rendered content</h1>
      <ClientComponent initialData={data} />
    </div>
  )
}

Data Fetching Strategies#

Static Data (Default)#

// Cached by default
async function getStaticData() {
  const res = await fetch('https://api.example.com/data')
  return res.json()
}

Dynamic Data#

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

Revalidated Data#

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

Parallel Data Fetching#

export default async function Page() {
  // Fetch in parallel
  const [posts, users] = await Promise.all([
    getPosts(),
    getUsers()
  ])
  
  return (
    <div>
      <Posts data={posts} />
      <Users data={users} />
    </div>
  )
}

Sequential Data Fetching#

export default async function Page() {
  const user = await getUser()
  const posts = await getUserPosts(user.id) // Waits for user
  
  return <UserPosts user={user} posts={posts} />
}

Route Handlers#

Basic API Route#

// app/api/posts/route.js
export async function GET(request) {
  const posts = await getPosts()
  return Response.json(posts)
}

export async function POST(request) {
  const body = await request.json()
  const post = await createPost(body)
  return Response.json(post, { status: 201 })
}

Dynamic Route Handlers#

// app/api/posts/[id]/route.js
export async function GET(request, { params }) {
  const post = await getPost(params.id)
  
  if (!post) {
    return Response.json({ error: 'Not found' }, { status: 404 })
  }
  
  return Response.json(post)
}

export async function PATCH(request, { params }) {
  const body = await request.json()
  const post = await updatePost(params.id, body)
  return Response.json(post)
}

export async function DELETE(request, { params }) {
  await deletePost(params.id)
  return new Response(null, { status: 204 })
}

Middleware Integration#

// app/api/protected/route.js
import { auth } from '@/lib/auth'

export async function GET(request) {
  const session = await auth(request)
  
  if (!session) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }
  
  const data = await getProtectedData(session.userId)
  return Response.json(data)
}

Advanced Patterns#

Loading States#

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

Error Handling#

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

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

Not Found Pages#

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

// Trigger not-found
import { notFound } from 'next/navigation'

export default async function Page({ params }) {
  const post = await getPost(params.slug)
  
  if (!post) {
    notFound()
  }
  
  return <Post data={post} />
}

Streaming with Suspense#

import { Suspense } from 'react'

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

export default function Page() {
  return (
    <div>
      <h1>Fast content</h1>
      <Suspense fallback={<div>Loading slow content...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}

Route Groups#

app/
├── (marketing)/
│   ├── layout.js          # Marketing layout
│   ├── page.js            # / route
│   └── about/
│       └── page.js        # /about route
└── (shop)/
    ├── layout.js          # Shop layout
    ├── products/
    │   └── page.js        # /products route
    └── cart/
        └── page.js        # /cart route

Parallel Routes#

app/
└── dashboard/
    ├── @analytics/
    │   └── page.js
    ├── @team/
    │   └── page.js
    └── layout.js
// app/dashboard/layout.js
export default function Layout({ children, analytics, team }) {
  return (
    <div>
      {children}
      <div className="grid grid-cols-2">
        {analytics}
        {team}
      </div>
    </div>
  )
}

Intercepting Routes#

app/
└── photos/
    ├── [id]/
    │   └── page.js        # /photos/123
    └── (..)photos/
        └── [id]/
            └── page.js    # Intercepts /photos/123 when navigating from same level

Best Practices#

1. Server Components by Default#

Use server components unless you need interactivity, browser APIs, or state.

2. Fetch Data Where You Need It#

Don't prop drill - fetch data in the component that needs it.

3. Use Streaming for Better UX#

Wrap slow components in Suspense to stream content progressively.

4. Optimize Images#

import Image from 'next/image'

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority
/>

5. Implement Proper Error Boundaries#

Create error.js files at appropriate levels in your route hierarchy.

6. Use Metadata API#

export const metadata = {
  title: 'My Page',
  description: 'Page description',
  openGraph: {
    title: 'My Page',
    description: 'Page description',
    images: ['/og-image.jpg'],
  },
}

Performance Optimization#

Code Splitting#

import dynamic from 'next/dynamic'

const DynamicComponent = dynamic(() => import('./HeavyComponent'), {
  loading: () => <p>Loading...</p>,
  ssr: false, // Disable SSR if needed
})

Font Optimization#

import { Inter } from 'next/font/google'

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

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

Route Prefetching#

Next.js automatically prefetches routes in the viewport using <Link>.

Conclusion#

The Next.js App Router provides a powerful foundation for building modern web applications. By understanding these patterns and best practices, you can create fast, scalable, and maintainable applications.

Key Takeaways#

  • Server Components are the default and provide better performance
  • Use Client Components only when needed for interactivity
  • Leverage streaming and Suspense for better UX
  • Fetch data where you need it, not at the top level
  • Use proper error handling and loading states
  • Optimize images, fonts, and code splitting

Next Steps#

  • Explore the Next.js documentation
  • Build a project using the App Router
  • Experiment with advanced patterns like parallel routes
  • Learn about deployment optimization

Related Guides:

Frequently Asked Questions

|

Have more questions? Contact us