DEV Community

Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on • Originally published at iloveblogs.blog

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

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
Enter fullscreen mode Exit fullscreen mode

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,
  }))
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Nested Layouts

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

Templates (Re-render on Navigation)

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

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Data Fetching Strategies

Static Data (Default)

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

Dynamic Data

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

Revalidated Data

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

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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} />
}
Enter fullscreen mode Exit fullscreen mode

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 })
}
Enter fullscreen mode Exit fullscreen mode

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 })
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

Advanced Patterns

Loading States

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

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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} />
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Parallel Routes

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

Intercepting Routes

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

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
/>
Enter fullscreen mode Exit fullscreen mode

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'],
  },
}
Enter fullscreen mode Exit fullscreen mode

Performance Optimization

Code Splitting

import dynamic from 'next/dynamic'

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

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:


Originally published at https://iloveblogs.blog

Top comments (0)