Next.js App Router
Next.js From Zero: Project Structure, Routing and Layouts
What Next.js adds to React, the app folder, pages, layouts, dynamic routes, links and navigation, and the files with special names.
On this page
- Words used in this lesson
- What Next.js adds to React
- Make a project and look at it
- The app folder: folders are URLs
- Layouts: shared screens that stay on
- Dynamic routes
- Links and navigation
- The special files
- A quick map of the whole thing
- Look inside: what the browser receives
- Common mistakes
- Try it
- Interview questions
- Key takeaways
- What's next
In one line: Next.js is React plus a router, a server and a build system. You make a folder inside app/, put a page.tsx in it, and that folder becomes a URL. Files with special names (layout, loading, error, not-found) add shared screens, loading states and error screens without any wiring. Learn those few file names and you can read almost any Next.js project.
React by itself only draws components in the browser. It does not know about URLs, servers, or how to split your code into files that load fast. You have to add all of that yourself. Next.js adds it for you, and it decides a lot of things so you do not have to. The price is that you must learn its rules. This lesson teaches the first and most important rules: the project folders, how files become routes, layouts, dynamic routes, links, and the special files.
This is lesson 0 of ten. You should know basic React (components, props, JSX). If you do not, start with Thinking in React. The next lesson, Server Components and Client Components, explains the biggest idea in this version of Next.js: which code runs on the server and which runs in the browser. Here we stay on the map: where files go and what URL they make.
What I ran / checked. Everything ran on Linux with Node.js v22.22.0, Next.js 16.3.5 (the version this repo uses; the npm registry already had 16.4.0 when I wrote this, so a fresh install may be one step newer), React 19.3.0, TypeScript 7.0.2 (what npm gave me as latest) and Turbopack, the default bundler. I made a throwaway app in /tmp, ran next build and next start, used curl for the HTML, and used Playwright 1.56.1 with Chromium for the click tests. I also ran create-next-app@16.3.5 --yes and pasted its real file list. Sources I read (in my own words below): the guides that ship inside the package, in node_modules/next/dist/docs/01-app/ ("Installation", "Project structure", "Layouts and pages", "Linking and navigating", "Fetching data" and the file-convention pages). This project's own AGENTS.md tells you to read those bundled docs first, because this Next.js has changes you may not know. I trust them over memory. Not verified: I did not open nextjs.org (I used the copy of the docs inside the package). I only tested the production build, not Windows or macOS. Timings are from one machine and will vary.
Words used in this lesson
- Next.js is a framework built on React. It adds routing, server rendering, data fetching, caching, and a build system.
- App Router is the routing system based on the
app/folder. It is the one this series teaches. The older one is the Pages Router (thepages/folder). - Route is a URL path your app answers, like
/blog/hello. - Segment is one piece of a URL path between slashes. In
/blog/hellothe segments areblogandhello. - Page is the file
page.tsx. It is the screen for one URL. A folder with no page file is not a public URL. - Layout is the file
layout.tsx. It wraps pages and nested layouts. It stays on screen when you move between its pages. - Root layout is the layout in
app/layout.tsx. It is required and must contain the<html>and<body>tags. - Dynamic segment is a folder name in square brackets like
[slug]. It matches any value in that place of the URL. - params is the object with the values of dynamic segments. In this version of Next.js it is a Promise, so you must
awaitit. - Static rendering (prerendering) means the page is built once at build time and served as ready HTML.
- Dynamic rendering means the page is built on the server for each request.
- Client-side navigation means moving to another page without reloading the whole browser page.
- Prefetching means loading a page in the background before the user clicks, so the click feels instant.
- Special files are files with fixed names (
loading,error,not-found,route) that Next.js treats in a particular way.
What Next.js adds to React
Think about what you need for a real website:
- A way to map URLs to screens (routing).
- HTML that arrives already filled with content, so the page shows fast and search engines can read it.
- A place to run server code (read a database, hide a secret key).
- A bundler that splits your JavaScript so users download only what a page needs.
- Image, font and metadata helpers.
With plain React you pick and connect all of these yourself: a router, a bundler, a server, and so on. Next.js ships them together and connects them. That is the whole idea. It is a set of decisions made for you.
Next.js has two routers. The Pages Router (the pages/ folder) is older and still supported. The App Router (the app/ folder) is newer and is where all new features go. This series is only about the App Router. If you see getServerSideProps or a pages/ folder in an old tutorial, that is the old way.
Check the version of every tutorial. Next.js changed a lot in versions 15 and 16. Some examples online still use old APIs: params read without await, a file called middleware.ts, or fetch that was cached by default. The docs bundled with your installed version are always correct for that version. This repo's AGENTS.md says the same thing in one line: read the guide in node_modules/next/dist/docs/ before you write code.
Make a project and look at it
The official way to start is create-next-app. I ran it with --yes, which accepts the defaults (TypeScript, Tailwind CSS, ESLint, App Router, Turbopack, and the @/* import alias).
npx create-next-app@16.3.5 demo --yes --use-npm --skip-installHere is the real list of files it made (I left out the .git folder):
.gitignoreAGENTS.mdCLAUDE.mdREADME.mdapp/favicon.icoapp/globals.cssapp/layout.tsxapp/page.tsxeslint.config.mjsnext-env.d.tsnext.config.tspackage.jsonpostcss.config.mjspublic/file.svg globe.svg next.svg vercel.svg window.svgtsconfig.jsonAnd its package.json:
{ "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "eslint" }, "dependencies": { "next": "16.3.5", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.3.5", "tailwindcss": "^4", "typescript": "^5" }}Notice two things. First, only three runtime packages: next, react, react-dom. Second, the scripts. These three are the ones you will use all the time:
| Command | What it does |
|---|---|
next dev | Starts the development server with fast refresh. Pages are rendered on demand and not cached, so you see changes at once. |
next build | Makes the optimized production build and prints the route table (we read one below). |
next start | Runs the production build. This is what you test before you deploy. |
The file AGENTS.md (and CLAUDE.md that points to it) is new. create-next-app adds it to guide AI coding tools to read the bundled docs. It is plain text and you can edit it.
The default stack is only the default. The project works without Tailwind, and you can use JavaScript instead of TypeScript. The only things that matter for routing are the app folder and its files.
The top-level folders and files
The docs list four top-level folders: app (the App Router), pages (the Pages Router), public (static files served as they are, like /next.svg), and an optional src (a folder that can hold app, if you want your code in src/). The top-level files that matter to us:
next.config.tsholds configuration (lesson 8).proxy.tsis code that runs before a request is handled (lesson 4). In older versions this file was calledmiddleware.ts..env.localholds local environment variables, and must not go into git (lesson 8).instrumentation.tssets up monitoring (lesson 8).
The app folder: folders are URLs
The main rule is short:
Folders inside app/ are URL segments. A folder becomes a public URL only when it has a page file (or a route file).
Let us build a small app and see. This is the layout I used for the experiments in this lesson:
app/ layout.tsx root layout, wraps everything page.tsx / not-found.tsx the 404 screen blog/ layout.tsx wraps /blog and everything under it page.tsx /blog [slug]/page.tsx /blog/hello-world, /blog/anything (marketing)/about/page.tsx /about (the (marketing) part is not in the URL) shop/[...parts]/page.tsx /shop/a, /shop/a/b/c docs/[[...rest]]/page.tsx /docs, /docs/a, /docs/a/b _components/secret.tsx not a route (private folder)The smallest page is a function that returns JSX, exported as the default export:
// app/page.tsxexport default function Page() { return <h1>Home</h1>}I ran next build on this app. The last part of the output is the route table, and it tells a lot:
Route (app)┌ ○ /├ ○ /_not-found├ ○ /about├ ○ /blog├ /blog/[slug]│ ├ ● /blog/hello-world│ └ ● /blog/second-post├ ƒ /docs/[[...rest]]└ ƒ /shop/[...parts]○ (Static) prerendered as static content● (SSG) prerendered as static HTML (uses generateStaticParams)ƒ (Dynamic) server-rendered on demandHow to read the symbols:
○Static. The page was built once at build time. A request just gets a ready file.●Also prerendered at build time, but for a dynamic route. It usedgenerateStaticParamsto list which values to build (we cover that below).ƒDynamic. Built on the server for each request.
Look at /docs/[[...rest]] and /shop/[...parts]. They are ƒ because I did not tell Next.js which values to build. It cannot guess, so it waits for a request. Keep this table in mind. You will read it after every build for the rest of this series.
Route groups and private folders
Two naming tricks help you keep order without changing URLs:
(name)in parentheses is a route group. The folder name is not part of the URL. I putaboutinside(marketing), and the URL is/about. Groups are useful to give a set of routes their own layout, or just to keep the folder tidy._namewith an underscore is a private folder. Next.js ignores it for routing.
Is the private folder needed? Not really. The docs point out that a folder without a page file is not routable anyway, so you can keep helper files next to your pages ("colocation"). I tested it with a file at app/_components/secret.tsx:
$ curl -s -o /dev/null -w "%{http_code}" localhost:3100/_components/secret404A 404, as expected. Private folders are useful to avoid a name clash with future special files and to keep UI code apart from routing code. Use them if you like the style.
Only the content returned by a page or route file is sent to the browser. A helper file sitting next to a page is not served. That is why colocation is safe.
Layouts: shared screens that stay on
A layout wraps its pages and stays on screen while you move between them. The root layout is required, lives at app/layout.tsx, and must contain <html> and <body>. Next.js has no index.html. This file is the shell of every page.
// app/layout.tsximport Link from 'next/link'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> )}children is the place where the page (or the next nested layout) goes. Layouts nest. If you add app/blog/layout.tsx, it wraps /blog and /blog/[slug], and the root layout wraps it:
// app/blog/layout.tsxexport default function BlogLayout({ children }: { children: React.ReactNode }) { return <section data-layout="blog"><p>Blog section</p>{children}</section>}This is the HTML I got from curl for /blog/hello-world. Read it from the outside in:
<body> <nav><a href="/">Home</a> | <a href="/blog">Blog</a> | <a href="/about">About</a></nav> <main> <section data-layout="blog"> <p>Blog section</p> <h1>Post: <!-- -->hello-world</h1> </section> </main></body>Root layout, then blog layout, then the page. (The <!-- --> is React's way to separate two pieces of text. It is harmless.)
Layouts keep their state
The docs say that on navigation a layout "preserves state, remains interactive and does not re-render". I wanted to see it, not just read it. I put a small button in the root layout. It is a Client Component with a counter (do not worry about the 'use client' line, lesson 1 explains it):
// app/ui/counter.tsx'use client'import { useState } from 'react'export default function Counter() { const [n, setN] = useState(0) return <button id="counter" onClick={() => setN(n + 1)}>clicks: {n}</button>}Then I drove a real Chromium with Playwright: click the button 3 times, click a <Link> to /blog, then click a plain <a href="/about"> link, and print the counter each time. I also counted how many full document requests the browser made.
before nav: clicks: 3 documents so far: 1after Link click to /blog: clicks: 3 documents: 1 url: http://localhost:3100/blogafter plain <a> to /about: clicks: 0 documents: 2This is the key result:
- With
<Link>, the counter stayed at 3 and no new document was loaded (still 1 document). The browser did not reload. Only the changed part was swapped in. - With a plain
<a>, the browser did a full page load (2 documents) and the counter went back to 0.
So layouts are not re-created when you navigate between pages they share. That is why a sidebar, a music player or a half-typed search box can survive navigation.
A layout cannot see which page is shown. A layout does not re-render on navigation, so it cannot read the current path in a simple way (the hooks that do that are Client Component hooks, like usePathname). If you need something that changes on every navigation, put it in the page, or use a template.tsx, which is like a layout but creates a fresh copy on each navigation.
Dynamic routes
A folder name in square brackets matches any value in that place. Three forms:
| Folder | Matches | Does not match |
|---|---|---|
blog/[slug] | /blog/a | /blog, /blog/a/b |
shop/[...parts] (catch-all) | /shop/a, /shop/a/b/c | /shop |
docs/[[...rest]] (optional catch-all) | /docs, /docs/a, /docs/a/b | nothing under another folder |
I tested these with curl against the production server. The <h1> of each page shows what params held:
/blog/hello-world 200 <h1>Post: hello-world</h1>/shop 404 <h1>Custom 404</h1>/shop/a/b 200 <h1>Shop: ["a","b"]</h1>/docs 200 <h1>Docs: null</h1>/docs/x/y 200 <h1>Docs: ["x","y"]</h1>The catch-all gives an array. The plain [...parts] does not match /shop alone (404), but [[...rest]] does, and then the value is undefined (I printed null).
params is a Promise
This is a change that trips up many people. In this version, params is a Promise. You must await it:
// app/blog/[slug]/page.tsxexport default async function Page(props: PageProps<'/blog/[slug]'>) { const { slug } = await props.params return <h1>Post: {slug}</h1>}PageProps<'/blog/[slug]'> is a global type helper. You do not import it. Next.js generates the types when you run next dev, next build or next typegen. It reads the route string and gives params the right shape. There is also LayoutProps for layouts.
What if you forget the await? I wrote params.slug without it. TypeScript caught it at build time:
app/syncp/[slug]/typed.tsx(2,22): error TS2339: Property 'slug' does not exist on type 'Promise<{ slug: string; }>'.Failed to type check.Good. That is another reason to use TypeScript with Next.js: the framework's own changes show up as type errors.
The same rule holds for searchParams (the ?q=shoes part of the URL). It is also a Promise, and reading it makes the page dynamic, because the value only exists when a request arrives. The docs suggest the searchParams prop when you need the value to load data on the server, and the client hook useSearchParams when you only need it in the browser.
generateStaticParams and dynamicParams
If a dynamic route could be built at build time, tell Next.js which values to build. You export generateStaticParams:
export function generateStaticParams() { return [{ slug: 'hello-world' }, { slug: 'second-post' }]}That is the reason /blog/hello-world and /blog/second-post have a ● in the route table. They were built to HTML files at build time.
What about a value that is not in the list? By default, Next.js builds it on demand the first time someone asks, then keeps it. I asked for /blog/nope:
/blog/nope 200 <h1>Post: nope</h1>It worked (200). If you want unknown values to be a 404 instead, set dynamicParams = false on the page. I made a second route to check:
// app/strict/[id]/page.tsxexport const dynamicParams = falseexport function generateStaticParams() { return [{ id: '1' }, { id: '2' }] }/strict/1 200/strict/3 404Use dynamicParams = false for a small fixed set of pages. Leave the default for things like products, where new ones appear all the time.
Links and navigation
Use the <Link> component from next/link for links between your own pages. It is a thin wrapper over <a>. You saw above what it gives you: client-side navigation, with no full reload. It also does prefetching: when a link is in view, Next.js loads that page in the background so the click feels instant. The docs say:
- For a static route, the full page is prefetched.
- For a dynamic route, prefetch is skipped, or only the part up to the nearest
loading.tsxis prefetched.
A plain <a> has none of this. The docs even show it in a code comment: <Link> is prefetched when it enters the viewport or is hovered, <a> is not. Use <a> for outside links and <Link> for inside links.
import Link from 'next/link'<Link href="/blog">Blog</Link><Link href={`/blog/${post.slug}`}>{post.title}</Link><Link href="/dashboard" prefetch={false}>Dashboard</Link> // turn prefetch offPrefetching has a cost. A page full of links (an endless table, for example) will prefetch many routes. The docs suggest prefetch={false} for such lists. You can prefetch on hover only, by switching prefetch from false to null in an onMouseEnter handler.
Other tools for moving around:
useRouter()(fromnext/navigation, in a Client Component) lets you navigate from code:router.push('/blog').redirect('/login')(fromnext/navigation) works on the server, for example inside a page or a Server Action.usePathname()reads the current path in a Client Component. It is how you highlight the active menu item.useLinkStatus()tells a<Link>child whether its navigation is still loading, so you can show a small spinner on slow networks.
In dev, a dynamic route without loading.tsx can feel stuck because the browser waits for the server. The fix is the next section.
The special files
Some file names have a fixed meaning. You already know page and layout. The others add behavior with almost no code.
| File | What it gives you |
|---|---|
loading.tsx | A loading screen shown while the page streams in. |
error.tsx | An error screen for the segment. It catches errors from its children. Must be a Client Component. |
global-error.tsx | The last-resort error screen for the root layout itself. |
not-found.tsx | The 404 screen. Also used when you call notFound(). |
template.tsx | Like a layout, but a new instance on each navigation. |
route.ts | An HTTP endpoint instead of a page (lesson 4). |
default.tsx | A fallback for parallel routes (an advanced topic we skip). |
These nest in a fixed order. The docs list it from the outside in: layout, template, error (an error boundary), loading (a Suspense boundary), not-found (an error boundary), then page. Each wraps the one after it. That is why an error screen shows inside your layout and the navigation stays visible.
loading.tsx
A loading.tsx next to a page wraps that page in a <Suspense> boundary for you. While the page waits for data, Next.js shows the loading file. I made a slow page and gave it a loading file:
// app/slow/page.tsxexport const dynamic = 'force-dynamic'export default async function Page() { await new Promise(r => setTimeout(r, 1500)) // pretend the database is slow return <h1>Slow page done</h1>}// app/slow/loading.tsxexport default function Loading() { return <p>Loading slow page...</p> }Then I asked for it with curl and printed the timing:
HTTP 200 total 1.530s time to first byte 0.081sThe first byte came after 0.08 seconds, but the whole response took 1.5 seconds. The server sent the shell and the loading text first, then the finished page when it was ready. In the HTML both strings are present: Loading slow page... and Slow page done. This is streaming. We study it in lesson 6. For now remember: loading.tsx is the cheapest way to make a slow page feel fast.
error.tsx
If a page throws, the nearest error.tsx catches it. It must be a Client Component, because it has a "Try again" button that runs in the browser:
// app/boom/error.tsx'use client'export default function Error({ error, reset }: { error: Error; reset: () => void }) { return <div><p>Something broke</p><button onClick={reset}>Try again</button></div>}I made a page that throws new Error('kaboom') and opened it in Chromium. The text on screen was:
Home | Blog | About | About (plain a)clicks: 0Something brokeTry againThe navigation and the counter from the root layout are still there, and only the page area was replaced. In the server log I saw the real error, with a digest (an id):
⨯ Error: kaboom digest: '2736574463'The browser never sees the message "kaboom" in production, only the digest, so private details do not leak. You search your logs for the digest to find the real error. The HTTP status of that response was 500.
not-found.tsx
Create app/not-found.tsx and it is used for any unmatched URL, and when you call notFound() from a page. I made one that returns <h1>Custom 404</h1>. All my 404 tests above (/shop, /zzz, /_components/secret) returned it with status 404.
A quick map of the whole thing
Put it together for one URL, /blog/hello-world:
RootLayout (app/layout.tsx) BlogLayout (app/blog/layout.tsx) [error boundary from error.tsx, if any] [Suspense from loading.tsx, if any] Page (app/blog/[slug]/page.tsx) params = { slug: "hello-world" }When you navigate to /blog/second-post, the two layouts are kept. Only the page part is replaced.
Look inside: what the browser receives
You can see what Next.js sends. A curl of a static page gives full HTML with the content already in it (the first load needs no JavaScript to show the text). Look at the response headers of the prerendered /blog/hello-world:
HTTP/1.1 200 OKx-nextjs-cache: HITx-nextjs-prerender: 1x-nextjs-stale-time: 300Cache-Control: s-maxage=31536000Content-Type: text/html; charset=utf-8x-nextjs-prerender: 1 and x-nextjs-cache: HIT say that this page was prerendered and served from the cache. s-maxage=31536000 lets a CDN keep it for a year (until the next deploy). This is what "static" means for performance: no code runs when the request arrives.
The same page can also be asked for in a second format, the RSC payload, which is what <Link> fetches for client-side navigation. In the build output you can see both next to each other: blog.html for the first visit and blog.rsc for navigation. Lesson 1 explains what is inside.
Common mistakes
1. A page.tsx and a route.ts in the same folder. A route handler and a page cannot share a segment. I tried it, and the build stopped with: Conflicting route and page at /conflict: route at /conflict/route and page at /conflict/page. Put API code under a different path, like app/api/....
2. No await on params. In this version params and searchParams are Promises. TypeScript tells you. In JavaScript you would silently get undefined.
3. Using <a> for internal links. It works, but you lose prefetching and keep none of your layout state. The counter test above shows the difference.
4. Expecting a dynamic route to be static. If you do not export generateStaticParams, a dynamic route shows as ƒ and runs on each request. Check the route table.
5. Putting error.tsx without 'use client'. Error boundaries have to be Client Components. Add the directive on the first line.
Try it
Make a fresh app with npx create-next-app@latest my-app --yes, then:
- Add
app/blog/page.tsxandapp/blog/[slug]/page.tsx. What URLs exist now? What doescurl localhost:3000/blog/hireturn? - Add
app/blog/layout.tsxwith a line of text. Where does that text show up: only on/blog, or on/blog/hitoo? - Make
app/(shop)/cart/page.tsx. What is the URL? - Make a folder
app/helpers/that only hasformat.ts. Is/helpersa route? - Add
app/blog/[slug]/loading.tsxand anawait new Promise(r => setTimeout(r, 2000))in the page. Click a<Link>to it. What do you see during the wait? - Run
npm run build. Is/blog/[slug]static or dynamic? AddgenerateStaticParamsreturning two slugs and build again. What changed in the table? - Throw an error in a page and add
error.tsx. Does the layout stay visible?
Answers
/blogand/blog/<anything>. For/blog/hiyou get the page withparams.slug = "hi"(status 200, if you have not setdynamicParams = false).- On both. A layout wraps every page below its folder.
/cart. The(shop)group does not appear in the URL.- No. A folder with no
pageorroutefile is not public. It is a safe place for helpers. - The loading UI appears at once, and the layout stays. When the wait ends, the real page replaces it.
- First it is
ƒ(dynamic). AftergenerateStaticParams, the two slugs show as●under/blog/[slug]. - Yes. The error screen replaces only the page area. The parent layouts stay.
Interview questions
What does Next.js add on top of React? (Must know)
React draws components. It does not give you routing, server rendering, a bundler setup, or a place for server code. Next.js adds those: a file-based router, server rendering and static generation, code splitting, data fetching on the server, caching, and helpers for images, fonts and metadata. It also gives you API endpoints through route handlers. In short, it is React plus the decisions you would otherwise make yourself.
How does routing work in the App Router? (Must know)
It is file-system based. Each folder inside app/ is a URL segment. A segment is public only when it contains a page file (or a route file). Dynamic parts use brackets, like [slug]. Layouts and other special files in a folder apply to that segment and everything below it. For example, app/blog/[slug]/page.tsx serves /blog/anything.
What is a layout, and how is it different from a page or a template? (Must know)
A page is the screen for one URL. A layout wraps pages and nested layouts through children, and it stays mounted when you navigate between the pages it wraps. So its state is kept and it does not re-render. In my test a counter in the root layout kept its value after a <Link> click and reset after a plain <a> click. A template looks like a layout but creates a fresh instance on each navigation, so it is for cases where you need a reset or an effect on every page change.
Why is params a Promise now, and what happens if I forget to await it? (Must know)
In this version params and searchParams are Promises, because their values may only be known at request time and Next.js wants to render as much as it can before then. You write const { slug } = await props.params. If you forget, TypeScript fails the build with "Property 'slug' does not exist on type Promise". In JavaScript you would get undefined without a warning, which is why I use TypeScript.
What is the difference between [slug], [...slug] and [[...slug]]? (Must know)
[slug] matches exactly one segment. [...slug] is a catch-all: it matches one or more segments and gives an array, but it does not match the bare parent path. [[...slug]] is an optional catch-all: it also matches the parent path, with slug undefined. I tested /shop against a catch-all and got 404, and /docs against an optional catch-all and got 200.
How does Next.js decide if a route is static or dynamic? (Senior signal)
I read the route table that next build prints. A route is prerendered (static) when nothing in it needs a request: no request-only data and nothing fetched at request time. Dynamic routes (ƒ) are built per request. A dynamic segment like [slug] is dynamic unless I export generateStaticParams to name the values to build. Using searchParams, cookies or headers also makes a page dynamic under the default model. Lesson 6 goes deeper into this decision.
What does generateStaticParams do, and what is dynamicParams? (Senior signal)
generateStaticParams returns the list of param values to build at build time. They appear with ● in the route table. dynamicParams controls what happens for values that are not in the list. By default (true) Next.js builds them on demand and caches them: my /blog/nope returned 200. With dynamicParams = false they return 404: my /strict/3 did. I use false for a small fixed set and the default for large changing sets.
When would you use <Link> and when <a>? (Must know)
I use <Link> for every navigation inside the app. It does client-side navigation without a full reload, keeps layout state, and prefetches routes when they come into view. I use a plain <a> for external sites and for things like file downloads. For long lists of links, I might set prefetch={false} to avoid many background requests.
What do loading.tsx and error.tsx do, and how do they relate to React? (Must know)
loading.tsx wraps the page in a <Suspense> boundary, and its content is the fallback. It is shown while the page waits for data, and it lets the layout appear at once. error.tsx wraps the segment in a React error boundary. Because error boundaries need client features, error.tsx must be a Client Component. It gets error and a reset function. In my test the error screen replaced only the page area, the layout stayed, and the status was 500 with only a digest shown to the user.
What are route groups and private folders for? (Trap)
A route group, (name), organizes routes and shares layouts without adding anything to the URL: (marketing)/about is /about. A private folder, _name, opts a folder out of routing. A common wrong answer is that private folders are needed to keep helper files from becoming routes. They are not. A folder is only a route when it has a page or route file, so colocation is already safe. I tested it: /_components/secret was a 404.
Can a page.tsx and a route.ts live in the same folder? (Trap)
No. Both would own the same URL and all its HTTP verbs. The build fails with "Conflicting route and page". A route file and a page can live at different paths, like app/page.tsx and app/api/route.ts.
Key takeaways
- Next.js is React plus routing, server rendering, bundling and more. This series uses the App Router (
app/), not the oldpages/. - Folders are URL segments. A folder is public only when it has a
page(orroute) file. - The root layout is required and holds
<html>and<body>. Layouts nest and keep their state when you navigate between their pages. I proved it with a counter. - Dynamic folders use brackets.
paramsandsearchParamsare Promises, so alwaysawaitthem.generateStaticParamsbuilds known values ahead of time, anddynamicParams = falsemakes the rest a 404. - Use
<Link>for internal links to get client-side navigation and prefetching. loading.tsx,error.tsxandnot-found.tsxgive you loading, error and 404 screens by file name.error.tsxmust be a Client Component.- Read the route table from
next build.○is static,●is prerendered withgenerateStaticParams,ƒis dynamic. - When you are unsure about this version, read the docs inside
node_modules/next/dist/docs/.
What's next
Next: Server Components and Client Components. Every component in the app folder is a Server Component by default, and that changes where your code runs and what the browser downloads. You will see the difference in a real bundle, learn what 'use client' really does, and learn how a careless prop can leak a password hash into the page. Practise now: make the small blog from this lesson and read the route table after next build.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- Slow First Load: Bundles, Code Splitting and LCP · Scenario Debugging
- Concurrent React, Suspense and Server Components in Next.js · Senior Frontend Depth
- Next.js Interview Questions · Full-Stack Questions
Part of the study guide
Related series