App Router Patterns
The App Router uses a file-system based routing convention inside the app/ directory.
Each folder represents a route segment, and special files control rendering behavior.
File Conventions
Every route segment can include these special files:
| File | Purpose | Required |
|---|---|---|
page.tsx | Defines the route UI | Yes (to make route accessible) |
layout.tsx | Shared UI that wraps children | No (inherits parent layout) |
loading.tsx | Loading skeleton shown during navigation | No |
error.tsx | Error boundary for the segment | No |
not-found.tsx | 404 UI for the segment | No |
template.tsx | Like layout but re-mounts on navigation | No |
Nested Layouts
Layouts persist across navigations and do not re-render. They are ideal for shared chrome like sidebars and headers.
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<aside className="w-64 border-r">
<nav>{/* sidebar links */}</nav>
</aside>
<main className="flex-1 p-6">{children}</main>
</div>
);
}
Route Groups
Use parentheses to organize routes without affecting the URL path:
app/
├── (marketing)/
│ ├── layout.tsx # Marketing layout
│ ├── page.tsx # Renders at /
│ └── about/page.tsx # Renders at /about
├── (dashboard)/
│ ├── layout.tsx # Dashboard layout
│ └── settings/page.tsx # Renders at /settings
Dynamic Routes
Use square brackets for dynamic segments:
// 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 content */}</article>;
}
Catch-All Routes
Use [...slug] for variable-length paths:
app/docs/[...slug]/page.tsx → /docs/a, /docs/a/b, /docs/a/b/c
Server vs Client Components
By default, every component in the App Router is a Server Component. Use the
"use client" directive only when you need browser APIs or interactivity.
// Server Component (default) — runs on the server
export default async function Page() {
const data = await fetch("https://api.example.com/data");
return <div>{/* render data */}</div>;
}
// Client Component — runs in the browser
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Keep
"use client"as low in the component tree as possible. Wrap only the interactive parts, not entire pages.