Topic 20.4
Next.js Basics: the App Router
In one line
Next.js is the most used React framework: in its App Router, folders inside app/ are URL segments, special files (page, layout, loading, error, not-found) build each route's UI, components are server components unless a file says "use client", and next/link and next/image handle fast navigation and optimised images.
Think of it like this
A school building. The building has one entrance hall every visitor passes through (the root layout). Each floor has its own corridor with notice boards (nested layouts). Each room is a page with a number on the door (the URL). If a room is being cleaned there's a "please wait" sign (loading), and if something breaks there's a "room closed" sign (error), but the corridor stays open. Next.js builds your site the same way from folders and files.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Next.js
- A React framework that adds routing, server rendering, Server Components, caching and build tooling.
- App Router
- Next.js's router based on the
app/folder and React Server Components. - Segment
- One part of a URL path, like
blogin/blog/hello; each folder inapp/is one. - Dynamic segment
- A folder like
[slug]that matches any value in that part of the URL and passes it asparams. - page.tsx
- The file that makes a folder a reachable route and holds that route's main UI.
- layout.tsx
- Shared UI that wraps every page below its folder and keeps its state between navigations.
- loading.tsx / error.tsx
- Files that wrap a route in a Suspense boundary and an error boundary automatically.
- Prefetching
- Loading a linked page's code and data in the background before the user clicks.
- Turbopack
- The bundler built into Next.js that compiles your code in development and for production.
Step by step
01Create the project
Run the official starter. Choose the recommended defaults (or --yes) and start the dev server.
$ npx create-next-app@latest my-app --yes
$ cd my-app
$ npm run dev
# then open http://localhost:300002Know the folder layout
Everything in app/ is routing. public/ holds files served as they are (/logo.png). next.config.ts holds framework options. A [slug] folder is a dynamic segment.
my-app/
├── app/
│ ├── layout.tsx # root layout: <html> and <body>, wraps everything
│ ├── page.tsx # /
│ ├── globals.css
│ └── blog/
│ ├── layout.tsx # wraps /blog and /blog/*
│ ├── page.tsx # /blog
│ ├── loading.tsx # Suspense fallback for this segment
│ ├── error.tsx # error boundary for this segment ("use client")
│ └── [slug]/
│ ├── page.tsx # /blog/hello-next, /blog/anything
│ └── not-found.tsx
├── public/
├── next.config.ts
└── package.json03Write the root layout and a page
The root layout renders the document. metadata sets the <title> and description for search engines. Pages are async server components that can load data directly.
import type { Metadata } from "next";
import Link from "next/link";
import "./globals.css";
export const metadata: Metadata = { title: "My blog", description: "Notes on React" };
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<nav>
<Link href="/">Home</Link> <Link href="/blog">Blog</Link>
</nav>
<main>{children}</main>
</body>
</html>
);
}04Add a dynamic route with params
params is a Promise in current Next.js. notFound() shows the nearest not-found.tsx. generateStaticParams lists slugs to prerender at build time.
import Image from "next/image";
import { notFound } from "next/navigation";
import { getPost, getPosts } from "@/lib/posts";
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((p) => ({ slug: p.slug }));
}
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<Image src={post.cover} alt={post.coverAlt} width={800} height={400} />
<p>{post.body}</p>
</article>
);
}05Add loading and error UI
These two files need no wiring: Next.js wraps the segment's page in the boundaries for you. error.tsx must start with "use client" because error boundaries are client components.
// app/blog/loading.tsx
export default function Loading() {
return <p>Loading posts...</p>;
}
// app/blog/error.tsx
"use client";
export default function Error({ error, retry }: { error: Error & { digest?: string }; retry: () => void }) {
return (
<div role="alert">
<h2>Something went wrong!</h2>
<button onClick={() => retry()}>Try again</button>
</div>
);
}06Build and read the route table
npm run build prints every route with a symbol: ○ static (prerendered), ● SSG with generateStaticParams, ◐ partial prerender (static shell plus streamed parts, with Cache Components), ƒ dynamic (rendered per request). Topic 20.5 explains how a route ends up in each group.
$ npm run build
...
Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /blog
└ ● /blog/[slug]
○ (Static) prerendered as static content
● (SSG) prerendered as static HTML (uses generateStaticParams)Try it yourself
- 1
Add a route
In the routing model, add
"app/about/page": () => <p>About us</p>and a/aboutbutton. Predict which layouts wrap it. Then add"app/blog/[slug]/layout"and check that only post pages get it. - 2
Remount instead of keep
Make the layout reset on navigation: in
App, render<div key={url}>{route(url)}</div>. Click the layout counter and switch pages. That's what atemplate.tsxdoes. - 3
Fail twice
In the loading/error model, make the fetch fail when
n <= 2. Predict what the screen shows after one retry, and add a second scripted click to recover.
Code & diagrams
A model of what the App Router does with your folders (the real one also handles route groups, parallel routes and more). Click **layout clicks**, then switch pages: the count stays, because the same layout stays mounted around a different page.
Preview
Console
Nothing logged yet.Expected console output
/ -> app/page {}
/blog -> app/blog/page {}
/blog/hello-next -> app/blog/[slug]/page {"slug":"hello-next"}The first fetch fails on purpose, the retry succeeds. `retry` in Next.js re-fetches and re-renders the segment, which this model imitates by creating a new promise. `onCaughtError: () => {}` only keeps React's caught-error log out of the console.
Preview
Console
Nothing logged yet.Expected console output
at 100 ms: Dashboard | Loading dashboard...
at 450 ms: Dashboard | Something went wrong: database timeout | Try again
at 600 ms: Dashboard | Loading dashboard...
at 950 ms: Dashboard | Visits today: 1200// app/ui/search.tsx
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function Search() {
const router = useRouter();
const [q, setQ] = useState("");
return (
<form onSubmit={(e) => { e.preventDefault(); router.push("/search?q=" + encodeURIComponent(q)); }}>
<input value={q} onChange={(e) => setQ(e.target.value)} aria-label="Search" />
</form>
);
}
// app/layout.tsx stays a server component and just renders <Search /> in its nav.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget "use client" in error.tsx
Write app/blog/error.tsx without the "use client" line.
Break #2
Read params without awaiting
In app/blog/[slug]/page.tsx, write export default function Page({ params }) { return <h1>{params.slug}</h1>; }.
Myth vs fact
Myth
Every component in the app/ folder is a server component, so I can't use useState.
Fact
Files are server components by default. Any file starting with "use client" is a client component and can use every hook.
Myth
<Link> is just a styled <a>.
Fact
It renders an <a> (so it works without JavaScript and is accessible), but it navigates on the client, keeps shared layouts mounted, and prefetches routes in production.
Myth
A folder inside app/ is always a URL.
Fact
Only folders with a page (or route) file are reachable. (group) folders don't appear in the URL and _private folders are never routes, so you can colocate components and helpers.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Layouts don't receive
searchParams, and they don't re-render on navigation between their child pages, so don't read the current path in a server layout expecting it to update. UseusePathname()in a small client component (for example, an active nav link). - ▸
PageProps<'/blog/[slug]'>andLayoutProps<'/dashboard'>are global helper types generated bynext dev,next buildornext typegen, soparamsare typed from your folder names. - ▸
In the Pages Router,
next/linkprefetch defaults differ; in the App Router, the defaultprefetch("auto") prefetches static routes fully and dynamic routes down to the nearestloading.tsx, which is another reason to add loading files. - ▸
This site (Hectal) is a Next.js 16 App Router app: its
src/app/layout.tsxis a server component, while interactive parts likesrc/components/ai/ai-tutor.tsxstart with"use client".
Remember this
- 1
Next.js is a React framework: it adds the parts React leaves out (Topic 0.1): routing, server rendering, Server Components and Functions, data caching, bundling (with Turbopack, its build tool), image and font optimisation. Version 16 is current. It has two routers: the older Pages Router (
pages/folder,getServerSideProps) and the App Router (app/folder), which is built on Server Components and is what new projects use. This topic covers only the App Router. - 2
Start a project with **
npx create-next-app@latest**. The recommended defaults give you TypeScript, ESLint, Tailwind CSS, the App Router, Turbopack and the@/*import alias;--yesaccepts them.npm run devstarts the dev server onhttp://localhost:3000,npm run buildmakes a production build and prints a route table, andnpm startserves that build. Next.js 16 needs Node.js 20.9 or newer. - 3
File-based routing: each folder inside
app/is a URL segment, and a route becomes public only when its folder has a **page.tsx** that default-exports a component.app/page.tsxis/,app/blog/page.tsxis/blog. A folder name in square brackets is a dynamic segment:app/blog/[slug]/page.tsxmatches/blog/anything, and the page receivesparams, which in Next.js 15+ is a Promise youawait:const { slug } = await params. Folders in parentheses like(marketing)group files without adding to the URL, and folders starting with_are private (never routes). Other files can live next to pages (colocation) without becoming routes. - 4
Layouts:
layout.tsxreceiveschildrenand wraps every page below its folder. The root layoutapp/layout.tsxis required and must render<html>and<body>. Layouts nest (the root layout wraps the blog layout wraps the post page) and, on navigation, a shared layout is not re-rendered or reset: its state, scroll position and inputs survive. A **template.tsx** is like a layout that remounts on every navigation, for when you do want a reset. - 5
Special files per folder:
loading.tsxis wrapped around the page in a Suspense boundary, so its UI shows instantly while the page's data loads (Topic 14.3).error.tsxwraps the segment in an error boundary (Topic 14.5); it must be a client component and receiveserrorand aretryfunction (Next.js 16.3 also passesreset, which re-renders without re-fetching).not-found.tsxshows when you callnotFound(), andglobal-error.tsxcatches errors in the root layout itself. Inside one folder they nest in a fixed order: layout > template > error > loading > not-found > page. - 6
Linking and images:
<Link href="/blog">fromnext/linkrenders an<a>, but navigates on the client (no full reload, layouts keep their state) and prefetches routes as links scroll into view (production only). UseuseRouter()fromnext/navigationfor navigation in code, and plain<a>for external sites.<Image>fromnext/imageextends<img>: it serves resized modern formats (like WebP) per device, lazy-loads below-the-fold images, and needswidthandheight(or a static import that provides them) so the page doesn't jump while it loads.altis required, as for any image (see /html). Server vs client in Next: pages and layouts are server components by default; add"use client"only to files that need state, effects, event handlers or browser APIs, such as a search box inside an otherwise server-rendered layout.
Explain it without notes
How does file-based routing work in the App Router? Include dynamic segments.
What is the difference between a layout, a template and a page?
What do loading.tsx and error.tsx do under the hood?
When does a component in a Next.js app need "use client"?
What does next/link add over a plain <a>, and what does next/image add over <img>?
Practice
Write the files for a docs section with URLs /docs and /docs/<topic>, a shared sidebar layout, and a loading state.
Write a not-found.tsx and the code in app/docs/[topic]/page.tsx that shows it for unknown topics.
Trade-offs
- ↔
Next.js gives you routing, SSR, RSC and caching in one package, but you adopt its conventions, its build tool and (for server features) a Node.js or serverless host.
- ↔
File-based routing is easy to read from the folder tree, but large apps get many small files per route; colocation and route groups keep it manageable.
Done when you can
I can create a Next.js app and know what
dev,buildandstartdo.I can map folders and
[dynamic]segments to URLs and readparams.I can write layouts, pages,
loading.tsx,error.tsxandnot-found.tsx.I can use
Link,useRouterandImagecorrectly.I can decide which Next.js files need
"use client".