Next.js App Router: The Complete Guide
The App Router is a paradigm shift in Next.js that brings powerful new features like layouts, nested routing, and enhanced server-side rendering capabilities. Let's dive deep into how to leverage these features effectively.
Understanding the App Directory Structure
The App Router uses a file-system based routing where folders define routes and special files define UI:
app/
├── layout.tsx # Root layout
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 page
└── dashboard/
├── layout.tsx # Dashboard layout
├── page.tsx # Dashboard page
├── settings/
│ └── page.tsx # Settings page
└── analytics/
└── page.tsx # Analytics page
Creating Layouts
Layouts are shared UI that wrap multiple pages:
// app/layout.tsx
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "My App",
description: "Generated by Next.js",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<nav className="bg-blue-600 p-4 text-white">
<h1>My Application</h1>
</nav>
<main className="container mx-auto p-4">{children}</main>
</body>
</html>
);
}
Nested Layouts
Create section-specific layouts that only apply to certain routes:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<aside className="min-h-screen w-64 bg-gray-100 p-4">
<nav>
<ul className="space-y-2">
<li>
<a href="/dashboard">Overview</a>
</li>
<li>
<a href="/dashboard/analytics">Analytics</a>
</li>
<li>
<a href="/dashboard/settings">Settings</a>
</li>
</ul>
</nav>
</aside>
<div className="flex-1 p-8">{children}</div>
</div>
);
}
Dynamic Routes
Create dynamic routes using square brackets:
// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
return (
<article>
<h1>Blog Post: {params.slug}</h1>
{/* Fetch and display blog content */}
</article>
);
}
// Generate static params for static generation
export async function generateStaticParams() {
const posts = await fetch("https://api.example.com/posts").then((res) =>
res.json(),
);
return posts.map((post: any) => ({
slug: post.slug,
}));
}
Server and Client Components
By default, components in the App Router are Server Components:
// app/posts/page.tsx (Server Component)
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
cache: "no-store", // This will fetch fresh data on every request
});
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>Latest Posts</h1>
{posts.map((post: any) => (
<article key={post.id} className="mb-6">
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
For interactive components, use the 'use client' directive:
// components/search-form.tsx (Client Component)
"use client";
import { useState } from "react";
export default function SearchForm() {
const [query, setQuery] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Handle search logic
console.log("Searching for:", query);
};
return (
<form onSubmit={handleSubmit} className="mb-6">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search posts..."
className="mr-2 rounded border p-2"
/>
<button
type="submit"
className="rounded bg-blue-500 px-4 py-2 text-white"
>
Search
</button>
</form>
);
}
Loading and Error States
Create better user experiences with loading and error boundaries:
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="h-32 w-32 animate-spin rounded-full border-b-2 border-gray-900"></div>
</div>
);
}
// app/dashboard/error.tsx
("use client");
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="py-8 text-center">
<h2 className="mb-4 text-2xl font-bold text-red-600">
Something went wrong!
</h2>
<p className="mb-4 text-gray-600">{error.message}</p>
<button
onClick={reset}
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
>
Try again
</button>
</div>
);
}
Data Fetching Patterns
The App Router provides several ways to fetch data:
// Static data fetching
async function getStaticData() {
const res = await fetch("https://api.example.com/data", {
cache: "force-cache", // This is the default
});
return res.json();
}
// Dynamic data fetching
async function getDynamicData() {
const res = await fetch("https://api.example.com/data", {
cache: "no-store",
});
return res.json();
}
// Revalidated data fetching
async function getRevalidatedData() {
const res = await fetch("https://api.example.com/data", {
next: { revalidate: 3600 }, // Revalidate every hour
});
return res.json();
}
Route Groups
Organize routes without affecting URL structure using parentheses:
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx
│ └── contact/
│ └── page.tsx
└── (shop)/
├── products/
│ └── page.tsx
└── cart/
└── page.tsx
Migration from Pages Router
To migrate from the Pages Router:
- Move pages from
pages/toapp/directory - Convert to layouts instead of
_app.tsx - Update data fetching from
getServerSideProps/getStaticPropsto async components - Add
'use client'directive to components that need client-side features
Best Practices
- Use Server Components by default - only add
'use client'when needed - Colocate related files - keep components, styles, and tests near the routes that use them
- Leverage layouts for shared UI and avoid prop drilling
- Use loading.tsx for better perceived performance
- Implement proper error boundaries with error.tsx files
- Optimize data fetching using appropriate caching strategies
The App Router represents the future of Next.js development, providing better performance, developer experience, and more intuitive patterns for building modern web applications.