App Router
The App Router is Next.js’s server-first routing system. Every component is a Server Component by default. You opt into client-side interactivity explicitly with "use client".
Mental Model
The App Router treats your app/ directory as a component tree, not a page tree. Each folder is a route segment, and special files control what renders at that segment. Layouts persist across navigations. Pages are the leaf content.
app/
layout.tsx # Root layout (wraps everything)
page.tsx # Home page (/)
blog/
layout.tsx # Blog layout (persists across blog pages)
page.tsx # Blog index (/blog)
[slug]/
page.tsx # Blog post (/blog/my-post)
loading.tsx # Loading skeleton for this route
error.tsx # Error boundary for this route
File Conventions
| File | Purpose | Required? |
|---|---|---|
page.tsx | UI for a route, makes segment publicly accessible | Yes (to create a route) |
layout.tsx | Shared UI that wraps children, persists across navigations | Yes (root only) |
loading.tsx | Instant loading skeleton via React Suspense | No |
error.tsx | Error boundary, catches errors in the segment and below | No |
not-found.tsx | Custom 404 for a segment | No |
route.ts | API endpoint (GET, POST, etc.) - cannot coexist with page.tsx | No |
template.tsx | Like layout but re-mounts on navigation (new instance per nav) | No |
default.tsx | Fallback for parallel routes when no match | No |
Root Layout
Every app needs a root layout. It must include <html> and <body> tags.
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Gotcha: The root layout replaces
_app.tsxand_document.tsxfrom the Pages Router. There are no_appor_documentfiles in App Router projects.
Nested Layouts
Layouts compose. A blog layout wraps all blog pages while the root layout wraps everything.
// app/blog/layout.tsx
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return (
<div className="blog-container">
<nav>Blog Navigation</nav>
<main>{children}</main>
</div>
);
}
The key property: layouts don’t re-render when you navigate between sibling pages. State in the blog layout persists as you move between /blog/post-1 and /blog/post-2.
Route Groups
Use parentheses to organize routes without affecting the URL structure:
app/
(marketing)/
about/page.tsx # /about
pricing/page.tsx # /pricing
layout.tsx # Shared marketing layout
(app)/
dashboard/page.tsx # /dashboard
settings/page.tsx # /settings
layout.tsx # Shared app layout
The (marketing) and (app) folders don’t create URL segments. They let you apply different layouts to different sections.
Dynamic Routes
// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}
Gotcha: In Next.js 15+,
paramsis a Promise. You mustawaitit. This was a breaking change from v14 where params was a plain object.
Catch-all and Optional Catch-all
app/docs/[...slug]/page.tsx # /docs/a, /docs/a/b, /docs/a/b/c
app/docs/[[...slug]]/page.tsx # same as above + /docs (index)
// app/docs/[...slug]/page.tsx
export default async function Docs({ params }: { params: Promise<{ slug: string[] }> }) {
const { slug } = await params;
// slug = ["a", "b"] for /docs/a/b
}
Parallel Routes
Render multiple pages in the same layout simultaneously using named slots:
app/
@analytics/page.tsx
@team/page.tsx
layout.tsx
page.tsx
// app/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<>
{children}
{analytics}
{team}
</>
);
}
API Routes (Route Handlers)
// app/api/posts/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET() {
const posts = await getPosts();
return NextResponse.json(posts);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const post = await createPost(body);
return NextResponse.json(post, { status: 201 });
}
Tip: For mutations from your own UI, prefer Server Actions over API routes. API routes are best for webhooks, third-party integrations, and public APIs.
Static vs Dynamic
Routes are static by default. Next.js makes a route dynamic when you use:
cookies()orheaders()searchParamsin a pageconnection()orunstable_noStore()- A
fetchcall withcache: "no-store"
You can force static generation with generateStaticParams:
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
Next: Server Components | Caching