AI Reading
Quick summary of this article
Master the App Router file conventions in Next.js 16: pages, nested layouts, route groups, the Link component and active link highlighting with TypeScript.
- Nested layouts let you share a header, sidebar or footer across routes without re-rendering it on every navigation.
loading.tsxanderror.tsxgive you instant loading skeletons and error boundaries for free at any route level.- The
Linkcomponent prefetches routes in the viewport, andusePathname()highlights the active nav item. - Route groups
(marketing)and(shop)let you organise files and apply different layouts without changing the URL.
Complete Next.js 16 Series — Module 2 of 12
Module 1 gave you one page. Real applications have dozens of routes sharing chrome — a header, a sidebar, a footer — and they need to feel instant when you click. The App Router solves both problems with file conventions rather than configuration.
The Seven File Conventions
Inside any folder under src/app/, these filenames have special meaning:
page.tsx— the route’s UI. A folder is only routable if it has one.layout.tsx— shared UI that wraps the page and its children. Preserved across navigation.loading.tsx— an instant loading skeleton shown while the page streams in.error.tsx— an error boundary for the route and its children.not-found.tsx— UI fornotFound()calls and unmatched URLs.template.tsx— like a layout, but it remounts on every navigation.route.ts— an HTTP handler (a REST endpoint), covered in Module 8.
The rule that trips people up: a folder with no page.tsx is not a route. src/app/dashboard/settings/ with only a component file returns a 404.
Building a Nested Layout
Let’s build a small application shell. Create this structure:
src/app/
├── layout.tsx
├── page.tsx
├── about/
│ └── page.tsx
└── dashboard/
├── layout.tsx
├── page.tsx
├── settings/
│ └── page.tsx
└── reports/
└── page.tsx
The URLs produced are /, /about, /dashboard, /dashboard/settings and /dashboard/reports.
Now the dashboard layout at src/app/dashboard/layout.tsx:
import Link from "next/link";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="mx-auto flex max-w-6xl gap-8 px-6 py-10">
<aside className="w-56 shrink-0">
<nav className="flex flex-col gap-1">
<Link href="/dashboard" className="rounded px-3 py-2 hover:bg-neutral-100">
Overview
</Link>
<Link href="/dashboard/reports" className="rounded px-3 py-2 hover:bg-neutral-100">
Reports
</Link>
<Link href="/dashboard/settings" className="rounded px-3 py-2 hover:bg-neutral-100">
Settings
</Link>
</nav>
</aside>
<section className="min-w-0 flex-1">{children}</section>
</div>
);
}
Then each page stays tiny — src/app/dashboard/reports/page.tsx:
export default function ReportsPage() {
return (
<div>
<h1 className="text-2xl font-bold">Reports</h1>
<p className="mt-2 text-neutral-600">Monthly performance summary.</p>
</div>
);
}
Why layouts matter more than you think
When you navigate from /dashboard/reports to /dashboard/settings, Next.js renders only the changed segment. The dashboard sidebar is not re-rendered, its scroll position is preserved, and any client-side state inside it survives — because React reconciles the shared layout instead of replacing it.
That also means a layout cannot access params of the page below it, and data fetched in a layout is not automatically shared with the page. Fetching is deduplicated per request, which we exploit in Module 6.
Client-Side Navigation and Active Links
<Link> is not a plain anchor. It prefetches routes as they enter the viewport, so the navigation feels instant.
import Link from "next/link";
To highlight the current page you need the pathname, which is a client hook. Create src/components/NavLink.tsx:
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
type Props = {
href: string;
children: React.ReactNode;
};
export default function NavLink({ href, children }: Props) {
const pathname = usePathname();
const isActive = pathname === href;
return (
<Link
href={href}
aria-current={isActive ? "page" : undefined}
className={`rounded px-3 py-2 text-sm transition ${
isActive
? "bg-neutral-900 text-white"
: "text-neutral-700 hover:bg-neutral-100"
}`}
>
{children}
</Link>
);
}
"use client" on line 1 marks this file as a Client Component. That is the entire opt-in mechanism, and Module 5 is dedicated to it.
Loading States That Feel Instant
Add src/app/dashboard/loading.tsx:
export default function Loading() {
return (
<div className="animate-pulse space-y-4">
<div className="h-8 w-48 rounded bg-neutral-200" />
<div className="h-4 w-full rounded bg-neutral-200" />
<div className="h-4 w-2/3 rounded bg-neutral-200" />
</div>
);
}
Next.js wraps the segment in a Suspense boundary automatically. The moment you click a slow dashboard page, the shell and skeleton appear immediately while the data resolves. No spinner libraries, no isLoading flags.
Error Boundaries per Segment
Add src/app/dashboard/error.tsx. It must be a Client Component:
"use client";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div role="alert" className="rounded border border-red-300 bg-red-50 p-6">
<h2 className="font-semibold text-red-800">Something went wrong</h2>
<p className="mt-1 text-sm text-red-700">{error.message}</p>
<button
onClick={reset}
className="mt-4 rounded bg-red-600 px-4 py-2 text-sm text-white hover:bg-red-700"
>
Try again
</button>
</div>
);
}
Errors thrown in /dashboard/* render here instead of crashing the whole app. The root layout keeps working, so users can still navigate away — a far better experience than a white screen.
Route Groups
Sometimes you want separate layouts without changing URLs. Wrap a folder name in parentheses:
src/app/
├── (marketing)/
│ ├── layout.tsx → public header + footer
│ ├── page.tsx → /
│ └── pricing/
│ └── page.tsx → /pricing
└── (app)/
├── layout.tsx → sidebar shell, requires auth
└── dashboard/
└── page.tsx → /dashboard
(marketing) and (app) do not appear in the URL. You get two independent layouts, and in Module 10 we use (app) to place every authenticated route behind one guard.
Two route groups cannot resolve to the same URL —
(a)/about/page.tsxand(b)/about/page.tsxis a build error.
template.tsx vs layout.tsx
Use template.tsx when the wrapper must reset on every navigation — for example a page-entry animation or a form whose state must clear between routes. It behaves like a layout but produces a new component instance each time, which is slower but sometimes exactly what you need.
Module 2 Recap
- Only folders containing
page.tsxare routable. - Nested layouts persist across navigation and protect shared state.
loading.tsxanderror.tsxgive you streaming skeletons and error boundaries per segment.- Route groups
(name)organise files and layouts without touching the URL. <Link>prefetches;usePathname()drives active-link styling and requires"use client".
Next up — Module 3: styling with Tailwind CSS 4, building a responsive layout, and dark mode using the class strategy.
Continue the Course
- Course index: Next.js 16 Tutorial – all 12 modules
- Previous: Module 1: Getting Started & Project Structure
- Next: Module 3: Tailwind CSS 4 & Dark Mode
