Back to Blog

Next.js App Router: Complete Guide to Modern React Development

Frontend Team
December 15, 2024
10 min read
Web Development

Next.js App Router: Complete Guide to Modern React Development

Next.js 13+ introduced the App Router, a revolutionary approach to building React applications. Let’s explore its features and best practices.

App Router Basics

File-based Routing

app/
├── page.tsx              # Home page (/)
├── about/
│   └── page.tsx          # About page (/about)
├── blog/
│   ├── page.tsx          # Blog list (/blog)
│   └── [slug]/
│       └── page.tsx      # Blog post (/blog/:slug)
└── dashboard/
    ├── layout.tsx        # Nested layout
    └── page.tsx          # Dashboard (/dashboard)

Basic Page

// app/page.tsx
export default function Home() {
  return (
    <main>
      <h1>Welcome to Next.js App Router</h1>
    </main>
  );
}

Server Components (Default)

Components are Server Components by default:

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

export default async function BlogPage() {
  const posts = await getBlogPosts();

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

Benefits of Server Components

  1. Zero JavaScript: No client bundle
  2. Direct backend access: Database, APIs
  3. SEO-friendly: Fully rendered HTML
  4. Faster initial load: Less JS to parse

Client Components

Use 'use client' for interactivity:

'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

When to Use Client Components

  • Event listeners (onClick, onChange)
  • State (useState, useReducer)
  • Effects (useEffect)
  • Browser APIs (localStorage, window)
  • Custom hooks

Layouts

Shared UI across routes:

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <nav>
          <a href="/">Home</a>
          <a href="/about">About</a>
        </nav>
        <main>{children}</main>
        <footer>© 2024</footer>
      </body>
    </html>
  );
}

Nested Layouts

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="dashboard">
      <aside>
        <nav>{/* Dashboard navigation */}</nav>
      </aside>
      <section>{children}</section>
    </div>
  );
}

Dynamic Routes

Single Dynamic Segment

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

// Generate static paths
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());

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

Multiple Dynamic Segments

// app/shop/[category]/[product]/page.tsx
export default function ProductPage({
  params,
}: {
  params: { category: string; product: string };
}) {
  return (
    <div>
      <h1>Category: {params.category}</h1>
      <h2>Product: {params.product}</h2>
    </div>
  );
}

Catch-all Segments

// app/docs/[...slug]/page.tsx
export default function DocsPage({
  params,
}: {
  params: { slug: string[] };
}) {
  // /docs/a/b/c -> params.slug = ['a', 'b', 'c']
  return <h1>Docs: {params.slug.join('/')}</h1>;
}

Data Fetching

Parallel Data Fetching

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

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

export default async function Dashboard() {
  // Fetch in parallel
  const [user, posts] = await Promise.all([
    getUser(),
    getPosts(),
  ]);

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <PostList posts={posts} />
    </div>
  );
}

Sequential Data Fetching

export default async function Page() {
  // First fetch
  const user = await getUser();

  // Wait for user, then fetch posts
  const posts = await getUserPosts(user.id);

  return <div>{/* ... */}</div>;
}

Caching and Revalidation

// Cache for 1 hour
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 }
  });
  return res.json();
}

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

Loading and Error States

Loading UI

// app/blog/loading.tsx
export default function Loading() {
  return <div>Loading blog posts...</div>;
}

Error Handling

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

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

Streaming with Suspense

import { Suspense } from 'react';

async function Posts() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());
  return <PostList posts={posts} />;
}

export default function Page() {
  return (
    <div>
      <h1>Blog</h1>
      <Suspense fallback={<div>Loading posts...</div>}>
        <Posts />
      </Suspense>
    </div>
  );
}

Metadata and SEO

Static Metadata

// app/about/page.tsx
export const metadata = {
  title: 'About Us',
  description: 'Learn more about our company',
};

export default function About() {
  return <div>About page</div>;
}

Dynamic Metadata

// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`)
    .then(r => r.json());

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  };
}

Route Handlers (API Routes)

// app/api/users/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  const users = await db.users.findAll();
  return NextResponse.json(users);
}

export async function POST(request: Request) {
  const body = await request.json();
  const user = await db.users.create(body);
  return NextResponse.json(user, { status: 201 });
}

Dynamic Route Handlers

// app/api/users/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const user = await db.users.findById(params.id);
  return NextResponse.json(user);
}

Server Actions

// app/actions.ts
'use server';

export async function createPost(formData: FormData) {
  const title = formData.get('title');
  const content = formData.get('content');

  await db.posts.create({ title, content });
  revalidatePath('/blog');
}
// app/new-post/page.tsx
import { createPost } from '../actions';

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Create Post</button>
    </form>
  );
}

Best Practices

1. Server Components by Default

// ✅ Server Component (default)
async function UserProfile() {
  const user = await getUser();
  return <div>{user.name}</div>;
}

// Only use 'use client' when needed
'use client';
function InteractiveButton() {
  return <button onClick={() => alert('Hi!')}>Click</button>;
}

2. Composition Pattern

// Server Component
import ClientComponent from './ClientComponent';

export default async function ServerComponent() {
  const data = await fetchData();

  return (
    <div>
      <h1>Server Content</h1>
      <ClientComponent data={data} />
    </div>
  );
}

3. Optimize Images

import Image from 'next/image';

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1200}
      height={600}
      priority
    />
  );
}

4. Use TypeScript

interface PageProps {
  params: { slug: string };
  searchParams: { [key: string]: string | string[] | undefined };
}

export default async function Page({ params, searchParams }: PageProps) {
  // Fully typed!
}

Performance Tips

  1. Use Server Components: Reduce client-side JavaScript
  2. Streaming: Use Suspense for better UX
  3. Image Optimization: Use next/image
  4. Font Optimization: Use next/font
  5. Code Splitting: Automatic with dynamic imports

Conclusion

Next.js App Router represents the future of React development. By leveraging Server Components, streaming, and modern patterns, you can build faster, more SEO-friendly applications with better developer experience.

Start with Server Components by default, add Client Components only when needed, and embrace the streaming capabilities for optimal performance.

Next.jsReactApp RouterServer ComponentsWeb Development

Related Articles

Web Development

React Performance Optimization: Tips and Tricks for 2025

Master React performance optimization with modern techniques including memoization, code splitting, and concurrent features...

June 10, 2025
6 min
Read More
Web Development

Advanced TypeScript: Mastering Type System for Better Code

Explore advanced TypeScript features including generics, conditional types, and utility types to write safer and more maintainable code...

February 14, 2025
9 min
Read More
Web Development

CSS Grid vs Flexbox: When to Use Each Layout System

Master modern CSS layouts by understanding when to use Grid vs Flexbox with practical examples and real-world use cases...

October 20, 2024
7 min
Read More