Command Palette

Search for a command to run...

Hectal
PHASE 20Advanced ~26 min· topic 4 of 7

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 blog in /blog/hello; each folder in app/ is one.
Dynamic segment
A folder like [slug] that matches any value in that part of the URL and passes it as params.
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.

Terminalwhole filebash
$ npx create-next-app@latest my-app --yes
$ cd my-app
$ npm run dev

# then open http://localhost:3000

02Know 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.

Project treewhole filebash
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.json
Know the folder layoutdiagram
Rendering diagram…

03Write 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.

app/layout.tsxwhole filetsx
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.

app/blog/[slug]/page.tsxwhole filetsx
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 + app/blog/error.tsxwhole filetsx
// 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.

Terminal (abbreviated)whole filebash
$ 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. 1

    Add a route

    In the routing model, add "app/about/page": () => <p>About us</p> and a /about button. Predict which layouts wrap it. Then add "app/blog/[slug]/layout" and check that only post pages get it.

  2. 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 a template.tsx does.

  3. 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

File-based routing and nested layouts, simulated New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

/ -> app/page {}
/blog -> app/blog/page {}
/blog/hello-next -> app/blog/[slug]/page {"slug":"hello-next"}
loading.tsx and error.tsx are Suspense and an error boundary React 19+ New tab

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

Press Run to see the page.

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
A client island in a server layouttsx
// 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.

terminal
$ next dev (abbreviated)
── what you'll see ──
… app/blog/error.tsx must be a Client Component. Add the "use client" directive the top of the file to resolve this issue.

Break #2

Read params without awaiting

In app/blog/[slug]/page.tsx, write export default function Page({ params }) { return <h1>{params.slug}</h1>; }.

terminal
$ Browser
── what you'll see ──
The heading is empty (and TypeScript reports that `slug` doesn't exist on type `Promise<{ slug: string }>`).

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. Use usePathname() in a small client component (for example, an active nav link).

  • ▸

    PageProps<'/blog/[slug]'> and LayoutProps<'/dashboard'> are global helper types generated by next dev, next build or next typegen, so params are typed from your folder names.

  • ▸

    In the Pages Router, next/link prefetch defaults differ; in the App Router, the default prefetch ("auto") prefetches static routes fully and dynamic routes down to the nearest loading.tsx, which is another reason to add loading files.

  • ▸

    This site (Hectal) is a Next.js 16 App Router app: its src/app/layout.tsx is a server component, while interactive parts like src/components/ai/ai-tutor.tsx start with "use client".

Remember this

  1. 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. 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; --yes accepts them. npm run dev starts the dev server on http://localhost:3000, npm run build makes a production build and prints a route table, and npm start serves that build. Next.js 16 needs Node.js 20.9 or newer.

  3. 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.tsx is /, app/blog/page.tsx is /blog. A folder name in square brackets is a dynamic segment: app/blog/[slug]/page.tsx matches /blog/anything, and the page receives params, which in Next.js 15+ is a Promise you await: 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. 4

    Layouts: layout.tsx receives children and wraps every page below its folder. The root layout app/layout.tsx is 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. 5

    Special files per folder: loading.tsx is wrapped around the page in a Suspense boundary, so its UI shows instantly while the page's data loads (Topic 14.3). error.tsx wraps the segment in an error boundary (Topic 14.5); it must be a client component and receives error and a retry function (Next.js 16.3 also passes reset, which re-renders without re-fetching). not-found.tsx shows when you call notFound(), and global-error.tsx catches errors in the root layout itself. Inside one folder they nest in a fixed order: layout > template > error > loading > not-found > page.

  6. 6

    Linking and images: <Link href="/blog"> from next/link renders an <a>, but navigates on the client (no full reload, layouts keep their state) and prefetches routes as links scroll into view (production only). Use useRouter() from next/navigation for navigation in code, and plain <a> for external sites. <Image> from next/image extends <img>: it serves resized modern formats (like WebP) per device, lazy-loads below-the-fold images, and needs width and height (or a static import that provides them) so the page doesn't jump while it loads. alt is 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

01

How does file-based routing work in the App Router? Include dynamic segments.

02

What is the difference between a layout, a template and a page?

03

What do loading.tsx and error.tsx do under the hood?

04

When does a component in a Next.js app need "use client"?

05

What does next/link add over a plain <a>, and what does next/image add over <img>?

Practice

01

Write the files for a docs section with URLs /docs and /docs/<topic>, a shared sidebar layout, and a loading state.

02

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, build and start do.

  • I can map folders and [dynamic] segments to URLs and read params.

  • I can write layouts, pages, loading.tsx, error.tsx and not-found.tsx.

  • I can use Link, useRouter and Image correctly.

  • I can decide which Next.js files need "use client".