Next.js & the App Router
Reviewed & published by Brayan K
Next.js is the most popular React framework — it wraps React with file-based routing, server rendering, data fetching, and production optimizations. The App Router (the app/ directory) is its modern routing system, built on React Server Components, with nested layouts and colocated loading and error UI. In this lesson you'll learn how folders become routes, what the special files (page, layout, loading, error) do, and where server and client components fit.
Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Why a framework like Next.js sits on top of React
- File-based routing: folders become URL segments
- Dynamic routes with [slug] and how params arrive
- Special files: page, layout, loading, error
- Nested layouts that persist across navigation
- Server components by default; 'use client' for interactivity
1️⃣ File-Based Routing
There's no router config. Your app/ folder tree is the route tree. A folder with a page.tsx becomes a navigable URL; nesting folders nests URLs.
app/
├─ page.tsx -> /
├─ about/
│ └─ page.tsx -> /about
├─ blog/
│ ├─ page.tsx -> /blog
│ └─ [slug]/
│ └─ page.tsx -> /blog/:slug (dynamic)
└─ shop/
└─ [id]/
└─ reviews/
└─ page.tsx -> /shop/:id/reviewsThe demo models exactly this mapping — feed it a file path, get the URL it serves:
// In the App Router, your folder tree IS your routes. We model the mapping
// from a file path under app/ to the URL it serves.
function fileToRoute(filePath) {
// strip the leading "app" and the trailing "/page.tsx"
let route = filePath
.replace(/^app/, "")
.replace(/\/page\.tsx$/, "");
// [slug] folders are dynamic segments -> :slug
route = route.replace(/\[(\w+)\]/g, ":$1");
return route === "" ? "/" : route;
}
console.log(fileToRoute("app/page.tsx")); // /
console.log(fileToRoute("app/blog/page.tsx")); // /blog
console.log(fileToRoute("app/blog/[slug]/page.tsx")); // /blog/:slug
console.log(fileToRoute("app/shop/[id]/reviews/page.tsx")); // /shop/:id/reviews
// Folders -> URL segments. [param] -> dynamic. No route config anywhere.2️⃣ A Page and a Dynamic Route
A page is a default-exported component. For a dynamic segment like [slug], Next.js passes the matched value in params. Because pages are server components, you can await data right inside them — no useEffect.
// app/blog/[slug]/page.tsx -> /blog/hello-world
export default async function BlogPost({ params }) {
const { slug } = await params; // the dynamic segment
const post = await getPost(slug); // fetch on the server
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}
// Pre-render known slugs at build time (static generation):
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((p) => ({ slug: p.slug }));
}3️⃣ Special Files
The App Router gives each route segment a set of reserved filenames. Drop one in a folder and Next.js wires it up automatically — loading and error UI come for free.
| File | What it renders |
|---|---|
| page.tsx | the unique UI for that route |
| layout.tsx | shared shell wrapping the page and its children |
| loading.tsx | instant fallback while the segment's data loads (Suspense) |
| error.tsx | error boundary UI when the segment throws |
| not-found.tsx | UI for a 404 in that segment |
Match each special file to its job in the demo:
// 🎯 YOUR TURN — fill in each ___ then press "Try it Yourself".
// The App Router uses SPECIAL filenames for each piece of a route. Match
// the file to what it renders.
const purpose = {
// 1) the actual page UI for a route
"page.tsx": "___", // 👉 "the page UI"
// 2) shared shell wrapping a route and its children (nav, etc.)
"layout.tsx": "___", // 👉 "the layout shell"
// 3) shown instantly while the page's data is loading
"loading.tsx": "the loading UI",
// 4) shown when a route segment throws
"error.tsx": "the error UI",
};
console.log("page.tsx ->", purpose["page.tsx"]);
console.log("layout.tsx ->", purpose["layout.tsx"]);
console.log("loading.tsx->", purpose["loading.tsx"]);
console.log("error.tsx ->", purpose["error.tsx"]);
// ✅ Expected output:
// page.tsx -> the page UI
// layout.tsx -> the layout shell
// loading.tsx-> the loading UI
// error.tsx -> the error UI4️⃣ Nested Layouts
A layout.tsx wraps everything below it and — crucially — stays mounted as you navigate between sibling pages. The root layout renders <html> and <body>; nested layouts add section-specific chrome like a dashboard sidebar.
// app/layout.tsx — the root layout (required)
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header><nav>...</nav></header>
{children}
</body>
</html>
);
}
// app/dashboard/layout.tsx — wraps only /dashboard/* pages
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar />
<main>{children}</main>
</div>
);
}The demo models how a page is wrapped by its whole layout chain, root first:
// Layouts NEST: a page is wrapped by every layout from the root down to it.
// We model "rendering" a route as composing its layout chain around the page.
function render(layouts, page) {
// wrap the page with each layout, innermost last
return layouts.reduceRight(
(children, layout) => layout + "(" + children + ")",
page
);
}
// URL /dashboard/settings is wrapped by RootLayout > DashboardLayout
const layouts = ["RootLayout", "DashboardLayout"];
const page = "SettingsPage";
console.log(render(layouts, page));
// RootLayout(DashboardLayout(SettingsPage))
// The root layout (html/body, nav) stays mounted; only the inner part
// swaps as you navigate between sibling pages — that's nested layouts.5️⃣ Server vs Client Components
In the App Router, components are server components by default: they render on the server and ship zero JavaScript. When you need interactivity — state, effects, event handlers, browser APIs — add the "use client" directive at the top of the file.
// Server component (default) — runs on the server, no JS shipped
export default async function Page() {
const data = await db.query("SELECT ..."); // safe: secrets stay server-side
return <Dashboard data={data} />;
}
// Client component — opt in for interactivity
"use client";
import { useState } from "react";
export default function Counter() {
const [n, setN] = useState(0); // hooks need a client component
return <button onClick={() => setN(n + 1)}>{n}</button>;
}Reorder Challenge
These lines of a dynamic blog page are scrambled. Put them in the correct order:
A. const post = await getPost(slug);
B. export default async function Post({ params }) {
C. return <article><h1>{post.title}</h1></article>;
D. const { slug } = await params;
E. }Quick Recall
1) What URL does app/products/[id]/page.tsx serve?
/products/:id — a dynamic route. The matched id arrives in params.
2) Are App Router components server or client by default?
Server by default. They render on the server and ship no JS. Add "use client" at the top of the file to opt into interactivity (state, effects, handlers).
3) What's the benefit of a nested layout.tsx over re-rendering chrome on every page?
The layout stays mounted across sibling navigations, so shared UI (a sidebar, its scroll position, its state) persists and only the inner page swaps.
Common Errors (and the fix)
- “useState is not allowed in Server Components”: you used a hook in a server component. Add "use client" at the top, or move the interactive part to its own client component.
- A folder with no page.tsx isn't routable: the folder exists but the URL 404s. Routes need a page.tsx (or be reached via a layout's children).
- Forgetting the root layout: app/layout.tsx with <html> and <body> is required.
- Importing server-only code into a client component: database calls and secrets must stay in server components. Pass already-fetched data down as props.
📋 Quick Reference
| Concept | Example |
|---|---|
| Static route | app/about/page.tsx → /about |
| Dynamic route | app/blog/[slug]/page.tsx |
| Read params | const { slug } = await params |
| Shared shell | layout.tsx with {children} |
| Loading UI | loading.tsx (auto Suspense) |
| Interactivity | "use client" at top of file |
Mini-Challenge: Route the Folders
Given a few file paths under app/, log the URL each one serves. Reuse the routing logic — this is how every Next.js URL is decided.
function fileToRoute(filePath) {
let route = filePath.replace(/^app/, "").replace(/\/page\.tsx$/, "");
route = route.replace(/\[(\w+)\]/g, ":$1");
return route === "" ? "/" : route;
}
const paths = [
"app/page.tsx",
"app/dashboard/page.tsx",
"app/users/[id]/page.tsx",
];
// your code here: log fileToRoute(path) for each path🎉 Lesson Complete
- ✅ Next.js is a full framework around React (routing, SSR, data, optimization)
- ✅ The app/ folder tree IS your route tree
- ✅ [slug] folders create dynamic routes; params arrive in params
- ✅ Special files: page, layout, loading, error
- ✅ Server components by default; "use client" for interactivity
Practice quiz
What is Next.js?
- A CSS library
- A replacement for JavaScript
- A React framework adding routing, server rendering, data fetching, and optimizations
- A database
Answer: A React framework adding routing, server rendering, data fetching, and optimizations. Next.js is a full framework built around React, with file-based routing, SSR/SSG, and production optimizations.
What is the App Router?
- Next.js's modern routing system in the app/ directory, built on React Server Components
- A third-party routing library
- The old pages/ system
- A CSS router
Answer: Next.js's modern routing system in the app/ directory, built on React Server Components. The App Router uses the app/ directory, server components by default, nested layouts, and colocated UI files.
How does file-based routing decide URLs in the App Router?
- From a routes.js config file
- From a database
- From the component names
- The folder structure under app/ is the route tree; a folder with page.tsx becomes a URL
Answer: The folder structure under app/ is the route tree; a folder with page.tsx becomes a URL. Folders under app/ map to URL segments; you create folders and page files instead of registering routes.
What does a folder named [slug] under app/ create?
- A static route
- A dynamic route segment whose value arrives in params
- A layout
- An error page
Answer: A dynamic route segment whose value arrives in params. [slug] is a dynamic segment; the matched value is passed to the page in params.
Which special file renders the unique UI for a route?
- page.tsx
- layout.tsx
- loading.tsx
- error.tsx
Answer: page.tsx. page.tsx renders the unique UI for that route segment and makes the folder navigable.
What does layout.tsx do?
- Renders a 404 page
- Fetches data only
- Provides a shared shell that wraps the page and its children and stays mounted across navigation
- Defines CSS variables
Answer: Provides a shared shell that wraps the page and its children and stays mounted across navigation. A layout wraps everything below it and persists across sibling navigations; only the inner page swaps.
In the App Router, components are by default...
- Client components
- Server components that run on the server and ship zero JS
- Class components
- Static HTML
Answer: Server components that run on the server and ship zero JS. Files under app/ are Server Components by default, rendering on the server and shipping no JS for that component.
How do you make a component interactive (state, effects, event handlers)?
- Add 'use server' at the top
- Use a class component
- Wrap it in Suspense
- Add the 'use client' directive at the top of the file
Answer: Add the 'use client' directive at the top of the file. The 'use client' directive at the top of a file opts that component into being a client component with interactivity.
Which special file provides an instant fallback while a segment's data loads?
- error.tsx
- loading.tsx
- not-found.tsx
- page.tsx
Answer: loading.tsx. loading.tsx supplies an instant Suspense fallback shown while the segment is loading.
What URL does app/products/[id]/page.tsx serve?
- /products only
- /[id]
- /products/:id — a dynamic route with id in params
- /products/id
Answer: /products/:id — a dynamic route with id in params. The [id] folder makes a dynamic route /products/:id; the matched id arrives in params.
Continue this course
- Previous: Styling React with Tailwind CSS
- Next: React Server Components & Server Actions — The React 19 shift — less JavaScript, direct data access, and form actions
- Quick reference: React cheat sheet