The Next.js App Router is the modern way to build React applications with Next.js. It uses React Server Components by default, supports streaming, and provides a more intuitive file-based routing system. This guide covers everything you need to start building with the App Router.
File-based routing
In the App Router, the app directory defines your routes. Each folder represents a URL segment, and special files determine what renders at that route.
| File | Purpose |
|---|---|
page.tsx |
The UI for a route (makes the route publicly accessible) |
layout.tsx |
Shared UI that wraps pages and nested layouts |
loading.tsx |
Loading UI shown while page content loads |
error.tsx |
Error UI shown when something fails |
not-found.tsx |
UI for 404 errors |
route.ts |
API endpoint (replaces pages/api) |
Basic routes
app/
├── page.tsx # /
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/my-post
└── contact/
└── page.tsx # /contact
A folder without page.tsx doesn't create a route. You can use folders purely for organization.
Dynamic routes
Use square brackets for dynamic segments:
app/
├── blog/
│ └── [slug]/
│ └── page.tsx # /blog/anything
├── users/
│ └── [id]/
│ └── page.tsx # /users/123
Access the parameter in the page:
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// Fetch post by slug
return <article>Post: {slug}</article>;
}
Catch-all routes
For routes with multiple segments:
app/
└── docs/
└── [...slug]/
└── page.tsx # /docs/a, /docs/a/b, /docs/a/b/c
The slug parameter is an array: ['a', 'b', 'c'].
Optional catch-all with double brackets matches the parent too:
app/
└── docs/
└── [[...slug]]/
└── page.tsx # /docs, /docs/a, /docs/a/b
Layouts
Layouts wrap pages and persist across navigations. They don't re-render when navigating between pages they contain.
Root layout
Every app needs a root layout with <html> and <body>:
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<nav>Site Navigation</nav>
<main>{children}</main>
<footer>Site Footer</footer>
</body>
</html>
);
}
Nested layouts
Add layout files in route segments for nested layouts:
app/
├── layout.tsx # Root layout (all pages)
├── (marketing)/
│ ├── layout.tsx # Marketing layout
│ ├── page.tsx # /
│ └── about/
│ └── page.tsx # /about
└── dashboard/
├── layout.tsx # Dashboard layout
├── page.tsx # /dashboard
└── settings/
└── page.tsx # /dashboard/settings
Dashboard pages get both the root layout and dashboard layout.
Route groups
Parentheses create route groups that don't affect the URL:
app/
├── (marketing)/
│ ├── layout.tsx # Shared marketing layout
│ ├── page.tsx # / (not /marketing)
│ └── pricing/
│ └── page.tsx # /pricing
└── (app)/
├── layout.tsx # Shared app layout
└── dashboard/
└── page.tsx # /dashboard
Route groups let you organize code and share layouts without adding URL segments.
Server and client components
By default, all components in the App Router are React Server Components. They run only on the server.
Server component benefits
- Direct database access
- Secure environment variables
- No JavaScript sent to browser
- Async/await in component body
// This is a server component (default)
export default async function ProductList() {
const products = await db.query('SELECT * FROM products');
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
When to use client components
Add "use client" at the top of a file to make it a client component:
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Use client components when you need:
| Feature | Example |
|---|---|
| React state | useState, useReducer |
| Effects | useEffect, useLayoutEffect |
| Browser APIs | window, localStorage, navigator |
| Event handlers | onClick, onChange, onSubmit |
| Custom hooks with state | useForm, useQuery |
Composition pattern
Keep server components as parents, pass data to client components as props:
// app/products/page.tsx (server component)
import { ProductFilter } from './ProductFilter';
export default async function ProductsPage() {
const categories = await db.query('SELECT * FROM categories');
return (
<div>
<ProductFilter categories={categories} />
{/* Server-rendered product list */}
</div>
);
}
// app/products/ProductFilter.tsx (client component)
'use client';
export function ProductFilter({ categories }) {
const [selected, setSelected] = useState(null);
// Interactive filter UI
}
Data fetching
Server components can fetch data directly with async/await:
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }, // Revalidate every hour
}).then((res) => res.json());
return <PostList posts={posts} />;
}
Fetch options
Next.js extends fetch with caching options:
| Option | Behavior |
|---|---|
{ cache: 'force-cache' } |
Cache indefinitely (default) |
{ cache: 'no-store' } |
Don't cache, always fetch fresh |
{ next: { revalidate: 60 } } |
Cache for 60 seconds, then revalidate |
{ next: { tags: ['posts'] } } |
Tag for on-demand revalidation |
Database queries
Query databases directly in server components:
import { db } from '@/lib/db';
export default async function UsersPage() {
const users = await db.select().from(usersTable);
return <UserList users={users} />;
}
No API route needed. The query runs on the server during render.
Loading and error states
Loading UI
Add loading.tsx for automatic loading states:
// app/dashboard/loading.tsx
export default function Loading() {
return <div className="animate-pulse">Loading dashboard...</div>;
}
This shows while page.tsx loads, using React Suspense under the hood.
Error handling
Add error.tsx for error boundaries:
'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>
);
}
Error boundaries must be client components to use the reset function.
Not found
Add not-found.tsx for 404 states:
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h1>404</h1>
<p>Page not found</p>
</div>
);
}
Trigger it programmatically with notFound() from next/navigation.
Metadata
Export a metadata object or generateMetadata function:
// Static metadata
export const metadata = {
title: 'My Page',
description: 'Page description',
};
// Dynamic metadata
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
};
}
Metadata merges with parent layouts. Child metadata overrides parent fields.
Navigation
Use the Link component for client-side navigation:
import Link from 'next/link';
export default function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>
</nav>
);
}
For programmatic navigation, use the useRouter hook in client components:
'use client';
import { useRouter } from 'next/navigation';
export function LoginButton() {
const router = useRouter();
async function handleLogin() {
await login();
router.push('/dashboard');
}
return <button onClick={handleLogin}>Login</button>;
}
Summary
The App Router uses file-based routing with the app directory. Components are server components by default—add "use client" only when you need interactivity. Use layouts for shared UI, loading.tsx for loading states, and error.tsx for error boundaries. Fetch data directly in server components with async/await. The official Next.js documentation has complete API references and examples.
