Hien Phan
Hien Phan
Strategy
BlogProjectsAbout
StrategyBlogProjectsAbout

Apps

šŸŽÆEnglishšŸ“¹YouTubešŸš€Indie⚔Learnāš™ļøDashboard
Back to Writing

Understanding the Next.js App Router

Deep dive into Next.js 13+ App Router, Server Components, and the new paradigm for building web applications.

January 20, 2026•7 min read•Boilerplate Team
Next.js
Next.js
React
Server Components
Understanding the Next.js App Router

The App Router represents a paradigm shift in how we build Next.js applications. Let's explore the key concepts and patterns.

What's New in App Router

The App Router introduces React Server Components, streaming, and new conventions for routing, layouts, and data fetching. It's the default for new Next.js applications.

Server Components vs Client Components

Server Components (Default)

Server Components render on the server and send HTML to the client. They're the default in the App Router:

// app/dashboard/page.tsx - Server Component by default

import { db } from "@/lib/db";
import { Suspense } from "react";

async function getUserData(userId: string) {
  // Direct database access - no API needed!
  return db.user.findUnique({ where: { id: userId } });
}

async function DashboardStats() {
  const stats = await db.stats.findMany();

  return (
    <div className="grid gap-4 md:grid-cols-3">
      {stats.map((stat) => (
        <div key={stat.id} className="p-4 border rounded">
          <h3>{stat.name}</h3>
          <p className="text-2xl">{stat.value}</p>
        </div>
      ))}
    </div>
  );
}

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<div>Loading stats...</div>}>
        <DashboardStats />
      </Suspense>
    </div>
  );
}
No Interactivity in Server Components

Server Components can't use hooks (useState, useEffect), event handlers (onClick), or browser APIs. Use Client Components for interactivity.

Client Components

Client Components render in the browser and support interactivity. Add "use client" at the top of the file:

"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";

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

  return (
    <div>
      <p>Count: {count}</p>
      <Button onClick={() => setCount(c => c + 1)}>Increment</Button>
    </div>
  );
}

When to Use Each

Component Decision Guide

Use Server Components for:

  • Data fetching (database queries, API calls)
  • Static content and layouts
  • Sensitive operations (API keys, tokens)

Use Client Components for:

  • Event handlers (onClick, onChange)
  • React hooks (useState, useEffect)
  • Browser APIs (window, localStorage)
  • Third-party libraries that need browser context

File-Based Routing

The App Router uses a file-based routing system with colocation:

app/
ā”œā”€ā”€ (dashboard)/           # Route group (doesn't affect URL)
│   ā”œā”€ā”€ layout.tsx         # Shared layout for dashboard routes
│   ā”œā”€ā”€ page.tsx           # /dashboard
│   ā”œā”€ā”€ settings/
│   │   └── page.tsx       # /dashboard/settings
│   └── [id]/              # Dynamic segment
│       └── page.tsx       # /dashboard/123
ā”œā”€ā”€ blog/
│   ā”œā”€ā”€ page.tsx           # /blog
│   ā”œā”€ā”€ [slug]/            # Dynamic route
│   │   └── page.tsx       # /blog/my-post
│   └── tag/
│       └── [tag]/         # Catch-all segment
│           └── page.tsx   # /blog/tag/nextjs
ā”œā”€ā”€ layout.tsx             # Root layout (required)
ā”œā”€ā”€ page.tsx               # / (homepage)
ā”œā”€ā”€ loading.tsx            # Loading UI for root
ā”œā”€ā”€ error.tsx              # Error boundary for root
└── not-found.tsx          # 404 page for root

Route Groups

Use parentheses (name) for route groups - they organize files without affecting the URL:

// app/(dashboard)/layout.tsx

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex">
      <Sidebar />
      <main>{children}</main>
    </div>
  );
}

Dynamic Routes

Use square brackets [param] for dynamic segments:

// app/blog/[slug]/page.tsx

async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await fetchBlogPost(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

Routing Patterns Quick Reference

PatternURLExample
page.tsx/Homepage
about/page.tsx/aboutStatic route
[id]/page.tsx/123Dynamic route
[...slug]/page.tsx/a/b/cCatch-all route
[[...slug]]/page.tsx/ or /a/b/cOptional catch-all

Data Fetching

Async Components

Server Components can be async, making data fetching simple:

// app/users/page.tsx

async function UserList() {
  const users = await fetch("https://api.example.com/users").then(r => r.json());

  return (
    <ul>
      {users.map((user: User) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default function UsersPage() {
  return (
    <div>
      <h1>Users</h1>
      <UserList />
    </div>
  );
}

Fetch Options

Control caching with fetch options:

// Static data (cached indefinitely)
const data = await fetch("https://api.example.com/data", {
  cache: "force-cache",
});

// No caching (always fresh)
const data = await fetch("https://api.example.com/data", {
  cache: "no-store",
});

// Revalidate every 60 seconds
const data = await fetch("https://api.example.com/data", {
  next: { revalidate: 60 },
});

// Revalidate on demand by tag
const data = await fetch("https://api.example.com/data", {
  next: { tags: ["posts"] },
});

Parallel Data Fetching

Layouts and Templates

Root Layout

Every App Router application needs a root layout:

// app/layout.tsx

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

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

export const metadata: Metadata = {
  title: "My App",
  description: "Built with Next.js",
};

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

Nested Layouts

Layouts nest based on the file structure:

// app/(dashboard)/layout.tsx

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex">
      <Sidebar />
      <main>{children}</main>
    </div>
  );
}

// app/(dashboard)/settings/layout.tsx

export default function SettingsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <h1>Settings</h1>
      <Tabs>{children}</Tabs>
    </div>
  );
}

The settings page will have both layouts applied!

Layout vs Template

Key Difference

Layouts persist across navigations and preserve state. Templates create a new instance for each navigation, useful for animations or re-fetching data.

// layout.tsx - Preserves state
export default function Layout({ children }) {
  return <div>{children}</div>;
}

// template.tsx - Re-mounts on navigation
export default function Template({ children }) {
  return <div>{children}</div>;
}

Loading and Error States

Loading UI

Create a loading.tsx file to show loading state automatically:

// app/dashboard/loading.tsx

export default function Loading() {
  return (
    <div className="space-y-4">
      <div className="h-8 w-48 animate-pulse bg-muted rounded" />
      <div className="grid gap-4 md:grid-cols-3">
        {[1, 2, 3].map((i) => (
          <div key={i} className="h-32 animate-pulse bg-muted rounded" />
        ))}
      </div>
    </div>
  );
}

Error Boundaries

Create an error.tsx file to handle errors:

// app/dashboard/error.tsx

"use client";

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}
You're Ready for App Router!

The App Router provides a more intuitive way to build Next.js apps with better performance and developer experience. Start using Server Components by default, and opt into Client Components when needed.

Quick Reference

Previous

Complete Guide to Payment Integration with Stripe, Paddle, and LemonSqueezy

Next

TypeScript Best Practices for React Developers

Hien Phan
Hien Phan

Indie maker building in public. Ship fast, learn loud.

Navigation

  • Blog
  • Projects
  • About

Contact

harrisonphan5@gmail.com

Based in Vietnam šŸ‡»šŸ‡³

Ā© 2026 Hien Phan. All rights reserved.

Built withNext.js+Vercel