|$ curl https://forge-ai.dev/api/markdown?path=docs/nextjs/app-router
$cat docs/next.js-app-router.md
updated Recentlyยท40 min readยทpublished

Next.js App Router

โ—†Next.jsโ—†Routingโ—†App Routerโ—†Beginner to Advanced๐ŸŽฏFree Tools
What is the App Router?

The App Router is Next.js routing system introduced in version 13. It uses the app/ directory and is built on top of React Server Components. Unlike the older Pages Router, the App Router provides nested layouts, co-located data fetching, streaming with Suspense, and advanced routing patterns like parallel and intercepting routes.

Every folder inside app/ becomes a URL segment. Special files within those folders define the behavior of the route โ€” page.tsx for the UI, layout.tsx for shared wrapping UI, loading.tsx for loading states, and error.tsx for error boundaries.

โ„น

info

The App Router is the recommended routing system for all new Next.js projects. The Pages Router is still supported but will not receive new features.
File Conventions

The App Router defines several special file conventions. Each has a specific role and placement rules within the route tree.

page.tsx โ€” The Route UI

Defines the unique UI for a route. Without a page.tsx, the folder is a layout-only segment. A page receives params and searchParams as props.

app/blog/[slug]/page.tsx
TSX
1// app/blog/[slug]/page.tsx
2export default async function BlogPost({
3 params,
4 searchParams,
5}: {
6 params: Promise<{ slug: string }>;
7 searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
8}) {
9 const { slug } = await params;
10 const { version } = await searchParams;
11
12 return (
13 <article>
14 <h1>Post: {slug}</h1>
15 {version && <p>Viewing version: {version}</p>}
16 </article>
17 );
18}

layout.tsx โ€” Shared Layout

Wraps child pages and layouts. Layouts persist across navigations โ€” they do not remount when the user navigates between child routes. This makes them ideal for sidebars, navigation bars, and other persistent UI.

app/dashboard/layout.tsx
TSX
1// app/dashboard/layout.tsx
2import { Sidebar } from "@/components/sidebar";
3import { TopBar } from "@/components/topbar";
4
5export default function DashboardLayout({
6 children,
7}: {
8 children: React.ReactNode;
9}) {
10 return (
11 <div className="flex h-screen">
12 <Sidebar />
13 <div className="flex-1 flex flex-col">
14 <TopBar />
15 <main className="flex-1 overflow-auto p-6">
16 {children}
17 </main>
18 </div>
19 </div>
20 );
21}

loading.tsx โ€” Instant Loading UI

Automatically wraps the nearest page in a React Suspense boundary. The loading UI is streamed immediately while the page content resolves. This eliminates the need for manual loading state management in most cases.

app/dashboard/loading.tsx
TSX
1// app/dashboard/loading.tsx
2export default function DashboardLoading() {
3 return (
4 <div className="space-y-4 animate-pulse">
5 <div className="h-10 bg-gray-800 rounded w-1/4" />
6 <div className="grid grid-cols-3 gap-4">
7 {[1, 2, 3].map((i) => (
8 <div key={i} className="h-32 bg-gray-800 rounded" />
9 ))}
10 </div>
11 </div>
12 );
13}

error.tsx โ€” Error Boundary

Catches runtime errors in the route segment. Must be a Client Component with 'use client'. Receives an error prop and a reset function to retry rendering.

app/dashboard/error.tsx
TSX
1// app/dashboard/error.tsx
2"use client";
3
4import { useEffect } from "react";
5
6export default function DashboardError({
7 error,
8 reset,
9}: {
10 error: Error & { digest?: string };
11 reset: () => void;
12}) {
13 useEffect(() => {
14 // Log to error reporting service
15 console.error("Dashboard error:", error);
16 }, [error]);
17
18 return (
19 <div className="rounded-lg border border-red-500/20 p-6">
20 <h2 className="text-red-400 font-mono text-sm mb-2">
21 Something went wrong
22 </h2>
23 <p className="text-gray-400 text-xs mb-4">{error.message}</p>
24 <button
25 onClick={() => reset()}
26 className="px-4 py-2 bg-red-500/10 text-red-400 rounded text-xs font-mono"
27 >
28 Try again
29 </button>
30 </div>
31 );
32}

not-found.tsx โ€” 404 UI

Renders when notFound() is called or when no matching route is found. You can have different not-found pages at different levels of the route tree.

app/not-found.tsx
TSX
1// app/not-found.tsx โ€” Global 404
2import Link from "next/link";
3
4export default function NotFound() {
5 return (
6 <div className="flex flex-col items-center justify-center min-h-[60vh]">
7 <span className="text-6xl font-mono text-gray-700 mb-4">404</span>
8 <h2 className="text-xl font-mono text-gray-300 mb-2">
9 Page Not Found
10 </h2>
11 <p className="text-sm text-gray-500 mb-6">
12 The page you are looking for does not exist or has been moved.
13 </p>
14 <Link
15 href="/"
16 className="px-4 py-2 bg-[#00FF41]/10 text-[#00FF41] rounded text-sm font-mono"
17 >
18 Go Home
19 </Link>
20 </div>
21 );
22}
Dynamic Routes

Dynamic routes use bracket syntax in folder names to capture URL segments. The captured values are available as params in the page and layout components.

Single Dynamic Segment

app/blog/[slug]/page.tsx
TSX
1// app/blog/[slug]/page.tsx
2// Matches: /blog/hello-world, /blog/my-post
3
4export default async function BlogPost({
5 params,
6}: {
7 params: Promise<{ slug: string }>;
8}) {
9 const { slug } = await params;
10
11 return <h1>Blog post: {slug}</h1>;
12}

Catch-All Routes

app/docs/[...slug]/page.tsx
TSX
1// app/docs/[...slug]/page.tsx
2// Matches: /docs/a, /docs/a/b, /docs/a/b/c
3
4export default async function DocsPage({
5 params,
6}: {
7 params: Promise<{ slug: string[] }>;
8}) {
9 const { slug } = await params;
10 // slug = ["a", "b", "c"] for /docs/a/b/c
11
12 return (
13 <div>
14 <p>Path segments: {slug.join(" > ")}</p>
15 </div>
16 );
17}

Optional Catch-All Routes

app/shop/[[...slug]]/page.tsx
TSX
1// app/shop/[[...slug]]/page.tsx
2// Matches: /shop, /shop/a, /shop/a/b
3
4export default async function ShopPage({
5 params,
6}: {
7 params: Promise<{ slug?: string[] }>;
8}) {
9 const { slug } = await params;
10
11 // slug is undefined for /shop
12 // slug = ["electronics", "phones"] for /shop/electronics/phones
13
14 if (!slug) {
15 return <h1>All Products</h1>;
16 }
17
18 return <h1>Category: {slug.join(" > ")}</h1>;
19}

generateStaticParams

Pre-render dynamic routes at build time. Return an array of param objects and Next.js generates static HTML for each.

app/blog/[slug]/page.tsx
TSX
1// app/blog/[slug]/page.tsx
2export async function generateStaticParams() {
3 const posts = await fetch("https://api.example.com/posts").then(
4 (res) => res.json()
5 );
6
7 return posts.map((post: { slug: string }) => ({
8 slug: post.slug,
9 }));
10}
11
12// Optional: generate metadata per page
13export async function generateMetadata({
14 params,
15}: {
16 params: Promise<{ slug: string }>;
17}) {
18 const { slug } = await params;
19 const post = await getPost(slug);
20 return { title: post.title, description: post.excerpt };
21}
โ„น

info

Dynamic routes without generateStaticParams are server-rendered on every request. With it, they are pre-rendered at build time and served from the CDN. Use it for pages with known, finite URLs like blog posts or product pages.
Route Groups

Route groups organize routes into logical groups without affecting the URL. Wrap the folder name in parentheses. This is useful for applying different layouts to different sections of your app.

app-directory
TEXT
1app/
2โ”œโ”€โ”€ (marketing)/
3โ”‚ โ”œโ”€โ”€ layout.tsx # Marketing layout (large nav, hero)
4โ”‚ โ”œโ”€โ”€ pricing/
5โ”‚ โ”‚ โ””โ”€โ”€ page.tsx # /pricing
6โ”‚ โ””โ”€โ”€ features/
7โ”‚ โ””โ”€โ”€ page.tsx # /features
8โ”‚
9โ”œโ”€โ”€ (dashboard)/
10โ”‚ โ”œโ”€โ”€ layout.tsx # Dashboard layout (sidebar, topbar)
11โ”‚ โ”œโ”€โ”€ dashboard/
12โ”‚ โ”‚ โ””โ”€โ”€ page.tsx # /dashboard
13โ”‚ โ””โ”€โ”€ settings/
14โ”‚ โ””โ”€โ”€ page.tsx # /settings
15โ”‚
16โ””โ”€โ”€ (auth)/
17 โ”œโ”€โ”€ layout.tsx # Auth layout (centered card)
18 โ”œโ”€โ”€ login/
19 โ”‚ โ””โ”€โ”€ page.tsx # /login
20 โ””โ”€โ”€ register/
21 โ””โ”€โ”€ page.tsx # /register
app/(marketing)/layout.tsx
TSX
1// app/(marketing)/layout.tsx
2export default function MarketingLayout({
3 children,
4}: {
5 children: React.ReactNode;
6}) {
7 return (
8 <div>
9 <header className="bg-white shadow">
10 <nav className="max-w-7xl mx-auto px-4 py-4">
11 <a href="/" className="text-lg font-bold">MyApp</a>
12 <div className="flex gap-6">
13 <a href="/pricing">Pricing</a>
14 <a href="/features">Features</a>
15 </div>
16 </nav>
17 </header>
18 <main>{children}</main>
19 <footer className="bg-gray-50 py-8">
20 <p>&copy; 2026 MyApp</p>
21 </footer>
22 </div>
23 );
24}
โ„น

info

Route groups let you co-locate files that affect a section of your app. You can add loading.tsx, error.tsx, and not-found.tsx inside a route group to scope those states to that section only.
Parallel Routes

Parallel routes allow you to render multiple pages in the same layout simultaneously. Define named slots in the layout and create matching folders with@ prefix.

parallel-routes
TEXT
1app/
2โ”œโ”€โ”€ dashboard/
3โ”‚ โ”œโ”€โ”€ layout.tsx # Defines slots: @analytics, @team
4โ”‚ โ”œโ”€โ”€ page.tsx # Main content
5โ”‚ โ”œโ”€โ”€ @analytics/
6โ”‚ โ”‚ โ””โ”€โ”€ page.tsx # Analytics panel
7โ”‚ โ”œโ”€โ”€ @team/
8โ”‚ โ”‚ โ””โ”€โ”€ page.tsx # Team panel
9โ”‚ โ””โ”€โ”€ @analytics/
10โ”‚ โ””โ”€โ”€ error.tsx # Error boundary for analytics slot
app/dashboard/layout.tsx
TSX
1// app/dashboard/layout.tsx
2export default function DashboardLayout({
3 children,
4 analytics,
5 team,
6}: {
7 children: React.ReactNode;
8 analytics: React.ReactNode;
9 team: React.ReactNode;
10}) {
11 return (
12 <div className="grid grid-cols-3 gap-4 p-4">
13 <div className="col-span-2">{children}</div>
14 <div className="space-y-4">
15 <div className="rounded-lg border p-4">{analytics}</div>
16 <div className="rounded-lg border p-4">{team}</div>
17 </div>
18 </div>
19 );
20}

You can add a default.tsxfile in each slot to provide a fallback UI when the slot's URL does not match.

app/dashboard/@analytics/default.tsx
TSX
1// app/dashboard/@analytics/default.tsx
2// Shown when /dashboard is visited (analytics slot has no matching URL)
3export default function AnalyticsDefault() {
4 return (
5 <div className="text-gray-500 text-sm p-4">
6 Select a time range to view analytics.
7 </div>
8 );
9}
โ„น

info

Parallel routes are excellent for modals. Render a modal in one slot while the main content stays visible in another. When the user navigates to the modal's URL, the modal slot renders the modal content while the other slots remain mounted.
Intercepting Routes

Intercepting routes let you render a route as if the user navigated to it, while the current URL stays the same. Use the (...) or (..) syntax in the folder name.

intercepting-routes
TEXT
1app/
2โ”œโ”€โ”€ feed/
3โ”‚ โ”œโ”€โ”€ page.tsx # /feed โ€” grid of photos
4โ”‚ โ””โ”€โ”€ [id]/
5โ”‚ โ””โ”€โ”€ page.tsx # /feed/123 โ€” full page photo
6โ”‚
7โ”œโ”€โ”€ photo/
8โ”‚ โ””โ”€โ”€ [id]/
9โ”‚ โ””โ”€โ”€ page.tsx # /photo/123 โ€” full page photo (direct URL)
10โ”‚
11โ””โ”€โ”€ @modal/
12 โ””โ”€โ”€ (..)photo/
13 โ””โ”€โ”€ [id]/
14 โ””โ”€โ”€ page.tsx # Intercepts /photo/123 as modal
app/@modal/(..)photo/[id]/page.tsx
TSX
1// app/@modal/(..)photo/[id]/page.tsx
2// Intercepts /photo/123 when navigating from /feed
3// But renders as full page when navigating directly to /photo/123
4
5import { Modal } from "@/components/modal";
6import { PhotoDetail } from "@/components/photo-detail";
7
8export default async function PhotoModal({
9 params,
10}: {
11 params: Promise<{ id: string }>;
12}) {
13 const { id } = await params;
14
15 return (
16 <Modal>
17 <PhotoDetail id={id} />
18 </Modal>
19 );
20}
๐Ÿ”ฅ

pro tip

Intercepting routes work with parallel routes to create modals. The modal slot intercepts the route and renders a modal overlay, while the main content remains visible underneath. The URL updates so the modal is shareable via direct link.
Template & Default Files

template.tsx

Similar to layout but remounts on every navigation. Use it when you need re-mounting behavior like triggering animation entrance effects on route changes.

app/dashboard/template.tsx
TSX
1// app/dashboard/template.tsx
2export default function DashboardTemplate({
3 children,
4}: {
5 children: React.ReactNode;
6}) {
7 // This component remounts on every navigation
8 // Layout does NOT remount โ€” this is the key difference
9 return (
10 <div className="animate-fade-in">
11 {children}
12 </div>
13 );
14}

default.tsx

Provides a fallback UI for parallel route slots when the URL does not match any of the slot's sub-routes. Without it, an unmatched parallel route slot renders nothing.

app/dashboard/@analytics/default.tsx
TSX
1// app/dashboard/@analytics/default.tsx
2export default function AnalyticsFallback() {
3 return (
4 <div className="rounded-lg border p-4">
5 <p className="text-sm text-gray-500">
6 No analytics data for this view.
7 </p>
8 </div>
9 );
10}
Active Route Indicators

Use usePathname to highlight the active link in your navigation.

components/nav-link.tsx
TSX
1"use client";
2
3import Link from "next/link";
4import { usePathname } from "next/navigation";
5
6export function NavLink({ href, children }: {
7 href: string;
8 children: React.ReactNode;
9}) {
10 const pathname = usePathname();
11 const isActive = pathname === href || pathname.startsWith(href + "/");
12
13 return (
14 <Link
15 href={href}
16 className={
17 isActive
18 ? "text-[#00FF41] font-semibold"
19 : "text-gray-500 hover:text-gray-300"
20 }
21 >
22 {children}
23 </Link>
24 );
25}
Route Pattern Reference
PatternExample URLParams
[slug]/blog/hello-world{ slug: "hello-world" }
[...slug]/docs/a/b/c{ slug: ["a", "b", "c"] }
[[...slug]]/shop (or /shop/a/b){ slug: undefined } or { slug: ["a", "b"] }
(group)No URL changeN/A
@slotNo URL changeParallel route slot
(...slug)No URL change (intercept)Catch segments for intercepting
โœ“

best practice

Use route groups to organize your app by feature or section. Use parallel routes for dashboards and modals. Use intercepting routes for photo galleries and detail views that should work as both modals and full pages.
$Blueprint โ€” Engineering DocumentationยทSection ID: NEXTJS-02ยทRevision: 1.0