Routing & Layouts
page, layout, template, loading, error, not-found.
In Next.js, the file system isthe router. You don't register routes in a config. You don't write a route table. You make a folder called about, drop a page.tsxinside, and now /aboutworks. That's the entire idea of the App Router, and once it clicks, you stop thinking about routing at all.
The app/ directory
Every route lives under app/. A folder name becomes a URL segment. To turn a folder into an actual page, you add a special file with a reserved name. The most important ones are page.tsx, layout.tsx, loading.tsx,error.tsx, not-found.tsx, and template.tsx. Anything else is just a regular module and doesn't get routed.
Folder tree to URL
app/
āāā layout.tsx // wraps every page
āāā page.tsx // /
āāā about/
ā āāā page.tsx // /about
āāā blog/
ā āāā layout.tsx // wraps /blog/*
ā āāā page.tsx // /blog
ā āāā [slug]/
ā āāā page.tsx // /blog/anything
āāā dashboard/
āāā layout.tsx // wraps /dashboard/*
āāā page.tsx // /dashboard
āāā settings/
āāā page.tsx // /dashboard/settingspage.tsx is not routable. You can still have nested folders for organization (layouts, route groups, private folders prefixed with _) and they won't create URLs.page.tsx: the leaf
A page.tsxis the component that renders at that URL. By default it's a React Server Component, which means it runs on the server, can be async, and never ships to the browser as JavaScript.
export default function AboutPage() {
return (
<main>
<h1>About</h1>
<p>We make widgets.</p>
</main>
);
}Want async data? Just add async. No getServerSideProps, no getStaticProps, no special hooks. The function literally awaits.
async function getUsers() {
const res = await fetch("https://api.example.com/users");
return res.json();
}
export default async function UsersPage() {
const users = await getUsers();
return (
<ul>
{users.map((u) => <li key={u.id}>{u.name}</li>)}
</ul>
);
}layout.tsx: persistent wrappers
A layout wraps every page below it. Crucially, layouts do not re-renderwhen you navigate between sibling pages. That's the killer feature. Your sidebar state, scroll position, video player, and any client component state inside the layout stays alive as the user moves around.
// The root layout is required. It must include <html> and <body>.
import "./globals.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}Layouts nest. A dashboard/layout.tsx wraps every page under /dashboard/*, and the root layout still wraps all of that. Same composition rules as React, just spelled with files instead of imports.
template.tsx: the opposite of layout
A template.tsx looks identical to a layout but with one critical difference: it remountson every navigation. New DOM nodes, new state, new effects. You almost never need this, but it's the right tool when you want enter/exit animations or want effects to run on every route change.
export default function Template({ children }: { children: React.ReactNode }) {
// useEffect inside any client component below will re-run on every navigation
return <div className="page-enter">{children}</div>;
}loading.tsx: instant Suspense
Drop a loading.tsx next to a page and Next automatically wraps that route in a React Suspense boundary, using your file as the fallback. When the page does async work, the loading UI shows immediately while the server streams the real content.
export default function Loading() {
return <p>Loading posts...</p>;
}You can also use <Suspense> manually inside any component for finer-grained boundaries. loading.tsxis a convenience for the "whole route" case.
error.tsx: route-scoped error boundaries
When something throws inside a route segment, the nearest error.tsx renders instead. It receives the error and a reset function to try again.
"use client"; // error files must be client components
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something broke</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}"use client" directive is required at the top.not-found.tsx: 404 with style
Render this when a page calls notFound() or no route matches.
import Link from "next/link";
export default function NotFound() {
return (
<div>
<h2>404</h2>
<p>That page doesn't exist.</p>
<Link href="/">Home</Link>
</div>
);
}Trigger it from anywhere in a Server Component:
import { notFound } from "next/navigation";
const post = await getPost(slug);
if (!post) notFound();How they nest, visualized
For a request to /dashboard/settings, Next composes the tree top down. Imagine each layer wrapping the next:
<RootLayout>
<DashboardLayout>
<ErrorBoundary fallback={<DashboardError />}>
<Suspense fallback={<DashboardLoading />}>
<SettingsPage />
</Suspense>
</ErrorBoundary>
</DashboardLayout>
</RootLayout>Quiz
What's the difference between layout.tsx and template.tsx?
Recap
- Folder = URL segment.
page.tsxmakes a folder routable. - layout.tsx wraps every page below it and persists across navigation.
- template.tsx wraps like a layout but remounts on every navigation.
- loading.tsx is an automatic Suspense boundary;error.tsx is an automatic error boundary (must be a client component).
- not-found.tsx renders for 404s and calls to
notFound().