The App Router is the routing system every new Next.js project uses. It's built on React Server Components, supports nested layouts, streaming, and parallel routes — capabilities the old Pages Router could not offer.
Every page.tsx inside app/ becomes a URL.
| File | URL |
|---|---|
app/page.tsx | / |
app/about/page.tsx | /about |
app/blog/page.tsx | /blog |
app/(marketing)/pricing/page.tsx | /pricing |
app/dashboard/settings/page.tsx | /dashboard/settings |
A layout.tsx wraps a segment and persists across navigation between sibling pages — its state, scroll, and DOM nodes are kept.
1// app/dashboard/layout.tsx
2export default function DashboardLayout({ children }: { children: React.ReactNode }) {
3 return (
4 <div className="grid grid-cols-[240px_1fr]">
5 <Sidebar /> {/* persists when you navigate from /dashboard/courses → /dashboard/billing */}
6 <main className="p-8">{children}</main>
7 </div>
8 );
9}Layouts nest. The root layout (app/layout.tsx) is the only one that must include <html> and <body>.
Pages render the leaf segment. They receive params (dynamic segments) and searchParams (query string) — both as Promises in Next.js 15+.
1// app/blog/[slug]/page.tsx
2type Props = {
3 params: Promise<{ slug: string }>;
4 searchParams: Promise<{ tab?: string }>;
5};
6
7export default async function BlogPost({ params, searchParams }: Props) {
8 const { slug } = await params;
9 const { tab = "post" } = await searchParams;
10 const post = await getPost(slug);
11 return <article>{tab === "comments" ? <Comments id={post.id} /> : <Body post={post} />}</article>;
12}Why Promises? It lets Next.js stream the route shell before either is resolved when prerendering.
A template.tsx looks like a layout but does not persist. A new instance mounts on every navigation. Use when you need an entry animation or a fresh effect run per page view.
When you navigate to /dashboard/courses/intro/lessons/1, Next renders:
app/layout.tsx
app/dashboard/layout.tsx
app/dashboard/courses/layout.tsx
app/dashboard/courses/[slug]/layout.tsx
app/dashboard/courses/[slug]/lessons/[id]/page.tsx
Each layer can have its own loading.tsx and error.tsx — so a slow database call in the lesson page doesn't block the dashboard chrome from rendering.
Use next/link for client-side transitions. It prefetches the target route automatically when the link enters the viewport.
1import Link from "next/link";
2
3<Link href="/dashboard">Dashboard</Link>
4<Link href={`/courses/${course.slug}`} prefetch={false}>Don't prefetch</Link>1"use client";
2import { useRouter } from "next/navigation";
3
4export function LogoutButton() {
5 const router = useRouter();
6 return (
7 <button onClick={async () => {
8 await fetch("/api/auth/logout", { method: "POST" });
9 router.push("/login");
10 router.refresh(); // re-fetch server components on the next page
11 }}>Sign out</button>
12 );
13}1"use client";
2import { usePathname, useSearchParams } from "next/navigation";
3
4export function ActiveLink({ href, children }: Props) {
5 const pathname = usePathname();
6 const active = pathname === href;
7 return <Link href={href} className={active ? "font-bold" : ""}>{children}</Link>;
8}These hooks are client-only — call them inside files marked "use client".