LogoLogoLeo de Jesús
HomeAboutMy WorkContact
My Work
DevelopmentFeatured

Next.js 14 App Router: Complete Guide

Master the new App Router in Next.js 14 with this comprehensive guide covering routing, layouts, and advanced patterns.

Leo de JesúsJanuary 23, 202415 min read
Next.jsReactFull-StackPerformance

Next.js 14 App Router: Complete Guide

Next.js 14 introduced significant improvements to the App Router, making it more stable and feature-rich. This comprehensive guide will help you master the new routing system and build powerful applications.

Understanding the App Router

The App Router is built on React Server Components and provides a more intuitive file-based routing system. Unlike the Pages Router, it offers better performance and more flexible layouts.

Key Differences from Pages Router

  • Server Components by default: Better performance and SEO
  • Nested layouts: Share layouts between routes
  • Loading and error boundaries: Better UX with loading states
  • Streaming: Progressive page loading
  • Parallel routes: Render multiple pages simultaneously

File Conventions

Basic Routing

app/
├── page.tsx          # Home page (/)
├── about/
│   └── page.tsx      # About page (/about)
├── blog/
│   ├── page.tsx      # Blog listing (/blog)
│   └── [slug]/
│       └── page.tsx  # Dynamic blog post (/blog/[slug])
└── layout.tsx        # Root layout

Special Files

app/
├── layout.tsx        # Root layout
├── page.tsx          # Home page
├── loading.tsx       # Loading UI
├── error.tsx         # Error UI
├── not-found.tsx     # 404 page
├── global.css        # Global styles
└── favicon.ico       # Favicon

Layouts and Nested Layouts

Root Layout

Every app must have a root layout:

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

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

export const metadata = {
  title: 'My App',
  description: 'A Next.js 14 application',
}

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

Nested Layouts

Create layouts that apply to specific route segments:

// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="blog-container">
      <aside className="blog-sidebar">
        <h2>Blog Categories</h2>
        {/* Category navigation */}
      </aside>
      <article className="blog-content">
        {children}
      </article>
    </div>
  )
}

Server and Client Components

Server Components (Default)

Server Components run on the server and are rendered to HTML:

// app/posts/page.tsx
import { getPosts } from '@/lib/posts'

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

Client Components

Use "use client" directive for interactive components:

'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>
  )
}

Data Fetching Patterns

Server-Side Data Fetching

Fetch data directly in Server Components:

// app/users/page.tsx
async function getUsers() {
  const res = await fetch('https://api.example.com/users', {
    cache: 'no-store' // Always fetch fresh data
  })
  
  if (!res.ok) {
    throw new Error('Failed to fetch users')
  }
  
  return res.json()
}

export default async function UsersPage() {
  const users = await getUsers()
  
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  )
}

Caching Strategies

Control how data is cached:

// Revalidate every hour
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 3600 }
})

// Always fetch fresh data
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store'
})

// Cache indefinitely
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: false }
})

Loading and Error Handling

Loading UI

Create loading states for route segments:

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

Error Boundaries

Handle errors gracefully:

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

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

Not Found Pages

Custom 404 pages:

// app/not-found.tsx
import Link from 'next/link'

export default function NotFound() {
  return (
    <div className="not-found">
      <h2>Not Found</h2>
      <p>Could not find requested resource</p>
      <Link href="/">Return Home</Link>
    </div>
  )
}

Dynamic Routes

Dynamic Segments

Create dynamic routes with brackets:

// app/posts/[slug]/page.tsx
interface PostPageProps {
  params: { slug: string }
}

export default async function PostPage({ params }: PostPageProps) {
  const post = await getPost(params.slug)
  
  if (!post) {
    notFound()
  }
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Catch-All Routes

Handle multiple segments:

// app/docs/[...slug]/page.tsx
interface DocsPageProps {
  params: { slug: string[] }
}

export default function DocsPage({ params }: DocsPageProps) {
  const slug = params.slug.join('/')
  
  return (
    <div>
      <h1>Documentation: {slug}</h1>
      {/* Render documentation content */}
    </div>
  )
}

Advanced Patterns

Parallel Routes

Render multiple pages simultaneously:

// 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-content">{children}</div>
      <div className="sidebar">
        {analytics}
        {team}
      </div>
    </div>
  )
}

Intercepting Routes

Show modals for certain routes:

// app/@modal/(..)photo/[id]/page.tsx
export default function PhotoModal({
  params,
}: {
  params: { id: string }
}) {
  return (
    <div className="modal">
      <h1>Photo {params.id}</h1>
      {/* Modal content */}
    </div>
  )
}

Metadata and SEO

Static Metadata

Define metadata at build time:

// app/about/page.tsx
export const metadata = {
  title: 'About Us',
  description: 'Learn more about our company',
  openGraph: {
    title: 'About Us',
    description: 'Learn more about our company',
    images: ['/og-image.jpg'],
  },
}

export default function AboutPage() {
  return <div>About content</div>
}

Dynamic Metadata

Generate metadata based on data:

// app/posts/[slug]/page.tsx
interface PostPageProps {
  params: { slug: string }
}

export async function generateMetadata({ params }: PostPageProps) {
  const post = await getPost(params.slug)
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.image],
    },
  }
}

Performance Optimization

Streaming

Progressive page loading:

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

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

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

Code Splitting

Automatic code splitting with dynamic imports:

'use client'

import dynamic from 'next/dynamic'

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

export default function Page() {
  return (
    <div>
      <h1>My Page</h1>
      <HeavyComponent />
    </div>
  )
}

Migration from Pages Router

Step-by-Step Migration

  1. Create new app directory
  2. Move pages to app directory
  3. Update imports and exports
  4. Convert to Server Components where possible
  5. Update routing logic
  6. Test thoroughly

Common Migration Patterns

// Pages Router
export async function getServerSideProps() {
  const data = await fetchData()
  return { props: { data } }
}

// App Router
export default async function Page() {
  const data = await fetchData()
  return <div>{/* render data */}</div>
}

Conclusion

The Next.js 14 App Router represents a significant evolution in React-based web development. By mastering these concepts and patterns, you can build more performant, maintainable, and user-friendly applications.

Key takeaways:

  • Leverage Server Components for better performance
  • Use nested layouts for consistent UI
  • Implement proper loading and error states
  • Take advantage of automatic code splitting
  • Optimize for SEO with proper metadata

Start experimenting with these patterns in your projects, and you'll quickly see the benefits of the new App Router architecture.

Back to My Work
L3CHUGU1T4© 2026BlogContact