App Router Basics
The App Router uses a file-system based router where folders define routes and special files define UI and behavior.
Search across all documentation pages
The App Router uses a file-system based router where folders define routes and special files define UI and behavior.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Quick-reference recipe card - copy-paste ready.
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home route → /
├── loading.tsx # Loading UI for /
├── error.tsx # Error boundary for /
├── not-found.tsx # 404 UI for /
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── layout.tsx # Nested layout for /blog/*
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/:slug
└── api/
└── health/
└── route.tsx # GET /api/health
Key rule: A route is only publicly accessible when a folder contains a page.tsx or route.tsx file.
// app/layout.tsx - Root Layout (required, wraps every page)
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "My App",
description: "Built with Next.js App Router",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}// app/page.tsx - Home page (Server Component by default)
export default function HomePage() {
return (
<main>
<h1>Welcome</h1>
<p>This is a Server Component - no client JS shipped.</p>
</main>
);
}// app/dashboard/page.tsx - Nested route at /dashboard
export default function DashboardPage() {
return <h1>Dashboard</h1>;
}// app/api/health/route.tsx - Route Handler (API endpoint)
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ status: "ok", timestamp: Date.now() });
}When to reach for this: Every Next.js 15+ project uses the App Router. Start here when creating any new route, layout, or API endpoint.
app/ maps to a URL segment. app/blog/settings/page.tsx serves /blog/settings.page.tsx, layout.tsx, loading.tsx, error.tsx, not-found.tsx, route.tsx, template.tsx, and default.tsx."use client" at the top.route.tsx and page.tsx cannot coexist in the same folder. A route segment is either a page or an API route, not both.loading.tsx creates an automatic <Suspense> boundary. Next.js wraps the page in Suspense using loading.tsx as the fallback.error.tsx creates an automatic Error Boundary. It catches errors in the page and its children but not in the layout at the same level.// app/template.tsx - Like layout, but remounts on every navigation
export default function Template({ children }: { children: React.ReactNode }) {
return <div className="animate-fade-in">{children}</div>;
}// app/not-found.tsx - Global 404 page
export default function NotFound() {
return (
<div>
<h2>404 - Page Not Found</h2>
<p>The page you are looking for does not exist.</p>
</div>
);
}// app/api/users/route.tsx - Route handler with multiple methods
import { NextRequest, NextResponse } from "next/server";
export async function GET() {
const users = await db.user.findMany();
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
}// Next.js provides built-in types for page and layout props
// Page props in Next.js 15+ use Promise-based params
interface PageProps {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
// Layout props always include children
interface LayoutProps {
children: React.ReactNode;
params: Promise<{ slug: string }>;
}layout.tsx does not re-render on navigation. If you need fresh state on each navigation, use template.tsx instead.error.tsx does not catch errors in the same-level layout. To catch layout errors, place error.tsx in the parent segment.route.tsx must export named HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS). Default exports are ignored.await params inside pages and layouts - the synchronous API is deprecated.searchParams are also async in Next.js 15+. Use await searchParams or use(searchParams) - do not destructure synchronously.not-found.tsx in the root catches all unmatched routes. Segment-level not-found.tsx only triggers when you call notFound().| Approach | When to Use |
|---|---|
Pages Router (pages/ dir) | Legacy projects not yet migrated |
route.tsx handler | API-only endpoints with no UI |
template.tsx | Need fresh component instance on each navigation |
| Third-party router (TanStack Router) | Non-Next.js React apps needing type-safe routing |
A route is only publicly accessible when a folder contains a page.tsx or route.tsx file. Other files (components, utils, styles) colocated in the folder are not exposed as routes.
No. A route segment is either a page or an API route, never both. Place your route.tsx in a separate folder (e.g., app/api/health/route.tsx).
Server Components by default. You must add "use client" at the top of a file to make it a Client Component.
layout.tsx persists across navigations and does not remounttemplate.tsx remounts on every navigation, giving fresh state each timetemplate.tsx for enter/exit animations or per-navigation loggingThe error boundary created by error.tsx wraps the page, not the sibling layout. To catch layout errors, place error.tsx in the parent segment.
Next.js automatically wraps the page in <Suspense fallback={<Loading />}>. The loading UI shows instantly while the page streams in.
It breaks. Both params and searchParams are now Promise objects in Next.js 15+. You must await params inside pages and layouts. The synchronous API is deprecated.
// app/api/users/route.tsx
import { NextRequest, NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ users: [] });
}
export async function POST(request: NextRequest) {
const body = await request.json();
return NextResponse.json(body, { status: 201 });
}not-found.tsx catches all unmatched routes automaticallynot-found.tsx only triggers when you call notFound() from next/navigationinterface PageProps {
params: Promise<{ slug: string }>;
searchParams: Promise<{
[key: string]: string | string[] | undefined;
}>;
}
interface LayoutProps {
children: React.ReactNode;
params: Promise<{ slug: string }>;
}import { NextRequest, NextResponse } from "next/server";
export async function GET(): Promise<NextResponse> {
return NextResponse.json({ status: "ok" });
}
export async function POST(
request: NextRequest
): Promise<NextResponse> {
const body = await request.json();
return NextResponse.json(body, { status: 201 });
}It is ignored. route.tsx must export named HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS). Default exports do nothing.
Yes. Non-special files (components, utils, styles, tests) placed inside route folders are not exposed as routes. Only files with reserved names like page.tsx and route.tsx are treated as routes.
Reviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥