Skip to content
Chapter 29Lesson 1

The file system is the route table

Your first look at Next.js, how the App Router turns the folder tree under app/ into your app's URLs, and where every other file belongs.

The starter you’ll build on gives you a src/app/ folder with exactly two files: a layout.tsx and a page.tsx. Open the app and you see a page at /. Now product hands you the next ticket: ship /dashboard, and a /dashboard/invoices page underneath it.

In most frameworks you’d go looking for the routing config: a file with an array of route objects, a createRouter call, somewhere you register the URL and point it at a component. In Next.js there is no such file. There is no route table to edit, because the folder tree under app/ is the route table. You don’t register a route; you create a folder. By the end of this lesson you’ll be able to look at any app/ tree and read off the URLs it serves, take a target URL and create the files that serve it, and apply the one rule that decides where every other file in the project lives.

A folder is a URL segment, a page makes it a page

Section titled “A folder is a URL segment, a page makes it a page”

Two rules carry this whole system, and once they click everything else follows from them.

The first: every folder under app/ is a route segment , one slash-separated piece of the URL. A folder named dashboard contributes /dashboard, and a folder named invoices nested inside it contributes the invoices piece, giving /dashboard/invoices. The folder nesting is the URL nesting. There’s nothing to wire up.

The second: a segment is only visitable once it contains a page.tsx. A page.tsx is the file whose default-exported component renders at that URL. A folder with no page.tsx is perfectly legal; Next.js just won’t serve a page there. Visit it directly and you get a 404, because its only job is to hold deeper segments that do have a page.

So the mapping is mechanical. app/page.tsx, a page.tsx with no folder around it, is the index route, /. app/dashboard/page.tsx is /dashboard. app/dashboard/invoices/page.tsx is /dashboard/invoices. That’s the answer to the ticket: to ship those two routes you create src/app/dashboard/page.tsx and src/app/dashboard/invoices/page.tsx. No registration step exists because the files are the registration.

The diagram below makes the mapping literal. On the left is the app/ tree; on the right, the URL each page.tsx produces. Read it as a function: feed in a folder path ending in page.tsx, read out a URL.

  • Directory src/
    • Directory app/
      • page.tsx
      • Directory dashboard/
        • page.tsx
        • Directory invoices/
          • page.tsx
1 app/ page.tsx
renders at /
2 app/dashboard/ page.tsx
renders at /dashboard
3 app/dashboard/invoices/ page.tsx
renders at /dashboard/invoices

Every page.tsx is one URL. The folder path to it, minus the filename, is the path of the URL. The numbers tie each leaf to the route it serves.

Now the file itself. A page.tsx is the smallest thing that can be a page: a component, default-exported.

export default function DashboardPage() {
return <h1>Dashboard</h1>;
}

Three facts about this file matter, and each one tends to trip people up.

The export is default, and that is not optional. Next.js imports the default export of every page.tsx to find the component to render, and it has no other way to know which thing in the file is the page. This cuts against a rule you’ll see everywhere else in this course: prefer named exports. The App Router is one of the few places that rule yields, because the framework mandates a default export from its convention files: page.tsx, layout.tsx, route.ts, and a handful of others. So a default export is correct in these convention files and a code smell everywhere else. The files are the documented exception, not a loophole.

The component’s name is irrelevant to routing. Call it DashboardPage, Page, or Anything: Next.js never reads the name, only the default export and the folder it sits in. The folder names the route, and the file just provides the component. Name it for the humans reading the code, not for the router, which is why DashboardPage beats a bare Page.

It’s also a Server Component by default, with no 'use client' at the top. For now that means it renders on the server and ships no client-side JavaScript unless something asks for it. The next chapter covers what “Server Component” fully means; here, just note that page.tsx starts on the server side of that line.

Before anything gets layered on top, lock in the core mapping in both directions. The rest of the lesson assumes you can do this fluently.

Match each page.tsx path on the left to the URL it produces on the right.

Match each `page.tsx` file to the URL where it renders. Click an item on the left, then its match on the right. Press Check when done.

app/page.tsx
/
app/settings/page.tsx
/settings
app/settings/billing/page.tsx
/settings/billing
app/dashboard/page.tsx
/dashboard

Now reverse it, in your head: product wants a page at /team/members. Which files do you create? Work it out before you open the answer.

Answer

Two files: src/app/team/page.tsx (serves /team) and src/app/team/members/page.tsx (serves /team/members). If product only asked for /team/members and never /team, you’d still create the team/ folder, since it’s the segment that holds members/, but you could leave it without a page.tsx. Then /team would 404 while /team/members works.

The starter is the source of truth, not the wizard

Section titled “The starter is the source of truth, not the wizard”

Every Next.js tutorial, repository, and screenshot you’ll ever meet was scaffolded by one command: pnpm create next-app. It’s the official project generator, and you should be able to recognize what it produces, but in this course you won’t run it. The course ships its own starter, and that one is canonical. Knowing the wizard’s output matters only so its artifacts look familiar when you see them in the wild.

Here’s the shape it generates, using the same choices the course’s starter makes (Biome, a src/ directory), so this is also the tree you’ll be working in:

  • Directorysrc/
    • Directoryapp/
      • layout.tsx root layout, owns <html>/<body> (next lesson)
      • page.tsx the / route
      • globals.css global stylesheet, imported by the root layout
  • next.config.ts framework config: Turbopack, React Compiler, etc.
  • tsconfig.json TypeScript config, defines the @/* import alias
  • biome.json linter + formatter config
  • package.json
  • AGENTS.md instructions for coding agents

In Next.js 16 the wizard offers two paths. The fast one is a single keystroke for recommended defaults: TypeScript, ESLint, Tailwind v4, the App Router, and an AGENTS.md. The customize path asks a few more questions on top of those: which linter to use (ESLint, Biome , or none), whether to enable the React Compiler, whether to put your code under a src/ directory, and what import alias to use. Notice there’s no bundler question: Turbopack is the default in 16, with nothing to opt into.

The course’s starter picks Biome (the wizard now offers it as a first-class choice, not the default) and the src/ directory, which is why your tree is src/app/… rather than a bare app/… at the repo root. That’s the only structural difference you’ll notice: same routing rules, one extra folder at the top. The AGENTS.md in the template is there to steer coding agents toward current Next.js patterns rather than stale ones, a small nod to how this code actually gets written in 2026.

You can now create routes. The bigger question, the one that shapes the entire codebase rather than one URL, is where the rest of the code goes. The App Router has a strong, opinionated answer that’s worth treating as the first real architectural principle of this course.

Here’s a concrete case. The dashboard needs three supporting pieces, each used only by the dashboard: a RevenueChart component, a formatCents helper that turns 4250 into $42.50, and an archiveInvoice server action. Where do those three files go?

There are two answers, and the difference between them is what this section is about.

  • Directorysrc/
    • Directoryapp/
      • Directorydashboard/
        • page.tsx
    • Directorycomponents/
      • revenue-chart.tsx the dashboard’s
    • Directoryhooks/
      • use-dashboard-filters.ts the dashboard’s
    • Directorylib/
      • format-cents.ts the dashboard’s
    • Directoryservices/
      • archive-invoice.ts the dashboard’s
One feature scattered across five folders, each shared with every other feature. To understand the dashboard you open all five and pick its files out of the pile.

The first tab organizes by layer: a folder per kind of file, with all components together, all hooks together, all helpers together. It’s a structure you’ll recognize from older codebases, and it isn’t wrong everywhere. But for code that belongs to one route it’s the wrong default, and the second tab shows why: organizing by feature puts everything the dashboard needs inside app/dashboard/. One feature, one folder.

This is Architectural Principle #1: organize by purpose, not by file kind. A feature’s components, helpers, and actions live next to the page they serve, not in a components/ or hooks/ or services/ bucket on the other side of the repo.

You already have the instinct for this, even if you’ve never named it. When you write a function with a couple of private helpers, you put the helpers right next to it, not in a helpers.ts file three directories away, because the things that change together should sit together. The App Router extends that instinct to the file system. It’s especially natural here because the file system is already feature-shaped: it’s the route tree. Co-locating with the route means your folders mirror your product.

What’s at stake is change, which is most of what you do to a codebase after the first week. When the dashboard needs work, the by-feature layout gives you one folder to open to understand it and one folder to delete to remove it. The by-layer layout spreads a single feature’s whole lifecycle across five directories. Every change means hunting through all five for the relevant files, and deleting the feature cleanly means tracking down its scattered files one by one and hoping you found them all. The test is simple: to delete a feature, you should be able to delete its folder, and only one of these layouts passes it.

Private folders keep your files out of the router

Section titled “Private folders keep your files out of the router”

Co-location raises an obvious worry. If every folder under app/ is a route segment, doesn’t putting a revenue-chart.tsx next to page.tsx risk turning it into a URL? And if you make a charts/ folder to group a few of them, is /dashboard/charts now a broken half-page?

Two things resolve this, and the distinction between them is worth getting straight.

First, the safe default you already half-know: only page.tsx (and one other file we’ll meet shortly) creates a route. A bare revenue-chart.tsx sitting next to page.tsx is already non-routable, just a module you import and never a URL. Files in app/ are safely co-located by default, so a stray component file won’t leak as a page.

A folder, though, is a segment, so charts/ is a real concern, and that’s where the underscore comes in. A folder whose name starts with _ is invisible to the router. app/dashboard/_components/ and everything inside it opt out of routing entirely, so app/dashboard/_components/revenue-chart.tsx is reachable by import but can never be a URL. The underscore is the explicit “this is supporting code, not a route” marker.

So if bare files are already safe, why bother with the underscore at all? Three reasons, in ascending order of how much they matter:

  • Readability. Grouping the supporting files under _components/ and _lib/ keeps the route folder itself scannable: page.tsx and a couple of named folders, instead of a dozen loose files you have to sort by eye.
  • Convention. It’s what the ecosystem expects. Other developers and tooling read _components/ instantly as “co-located, non-routable.” Following the shared convention is free legibility.
  • Future-proofing. This is the reason that matters most. Next.js keeps adding reserved filenames. Suppose you wrote a component and named it default.tsx: Next.js would read it as a parallel-route fallback, a framework convention you’ll meet later in this chapter, and your component would quietly stop behaving as a plain module. A file at _lib/default.ts can never collide with a framework name, because the whole _lib/ folder is off the router’s radar.

Think of the underscore as cheap insurance, not a hard requirement. Here’s the canonical shape of a real feature folder, the structure you’ll reach for over and over for the rest of the course:

  • Directorysrc/
    • Directoryapp/
      • Directorydashboard/
        • page.tsx the route, /dashboard
        • layout.tsx the dashboard shell (next lesson)
        • Directory_components/ not a route, imported never visited
          • revenue-chart.tsx
          • kpi-card.tsx
        • Directory_lib/ not a route
          • format-cents.ts
        • _actions.ts not a route, the dashboard’s server actions

The canonical feature folder. Only the bold page.tsx is a URL; the _-prefixed folders and _actions.ts are invisible to the router, reachable by import, never a page.

A few conventions are baked into that tree, and they line up with the project’s code standards. Filenames are kebab-case (revenue-chart.tsx, format-cents.ts); the only exceptions are framework-mandated names like page.tsx and layout.tsx. The _actions.ts file is where a route’s server actions live; you’ll meet them properly much later, so for now just file it as “the dashboard’s server-side mutations.”

One more standard worth stating now: when you import from _lib/, you import the specific file you need, such as _lib/format-cents, and there is no index.ts that re-exports everything from the folder. Those re-export files are called barrels, and they’re banned in this course. A barrel pulls every file in the folder into the module graph at once, which defeats the careful Server/Client split you’ll spend the next chapter learning. Import the file, not the folder.

Co-location has a natural counter-question: surely not everything lives inside a route folder? A Button, a formatCents you end up needing in three different features, the database client: none of those is owned by any single route. Where do they go, and when?

One rule resolves every instance of this question:

Co-locate under _components/ or _lib/ until a second feature imports it. Then promote it to the top-level components/ or lib/.

The threshold is “used by two or more features.” A helper the dashboard alone uses stays in app/dashboard/_lib/. The moment the invoices page also needs it, it graduates to src/lib/ where both can reach it. Start local, promote on the second use. That single heuristic answers “where does this go” for shared code.

Here’s where those promotions land, the top-level shape you’ll work in for the rest of the course:

  • Directorysrc/
    • Directoryapp/ all routes live here
    • Directorycomponents/ app-wide UI, used by 2+ features
      • Directoryui/ shadcn primitives (Ch 027)
    • Directorylib/ shared helpers, used by 2+ features
      • utils.ts cn() and other tiny helpers
      • result.ts the Result type and its helpers
    • Directorydb/ the database layer (a later unit)
    • env.ts typed, validated env vars (a later chapter)
  • next.config.ts framework config
  • tsconfig.json defines the @/* alias
  • proxy.ts the request gate (a later chapter)

Don’t try to absorb that whole tree. Most of it is owned by chapters you haven’t reached: db/, env.ts, and proxy.ts all get their own lessons. The point of showing it now is orientation. app/ is routes, components/ is shared UI, lib/ is shared logic, and everything else is a labeled box you’ll open later. It’s a map, not a syllabus.

There’s one mechanical detail in those imports worth pinning down, because you’ll type it hundreds of times. Look at how a route reaches a shared helper.

src/app/dashboard/invoices/page.tsx
import { formatCents } from '../../../lib/format-cents';

Fragile. Count the dots: three levels up just to reach lib/. Move this page one folder deeper or one folder over and every ../ is now wrong. The path describes where the file is, not what it wants.

The @/ is an import alias . A single line in tsconfig.json maps @/* to your source root, and from then on @/lib/format-cents resolves the same way from anywhere in the project. The contract an experienced developer follows: @/ for everything that crosses a feature boundary, relative paths only for imports within a feature, from the same folder or a sibling. Inside app/dashboard/, the page imports its own chart with ./_components/revenue-chart. They’re one feature and they move together, so the relative path is correct and reads as “right here.” But the moment an import reaches out of the feature, to lib/, to components/, or to another route’s code, it goes through @/.

This dovetails with the import-ordering rule from the code standards: imports come in three groups, external packages first, then @/ aliases, then relative imports, separated by blank lines. So the alias isn’t just ergonomics; it’s the visual cue that says “this dependency lives outside my feature.”

You now have both halves of the placement decision: co-locate by feature, promote on the second use. The skill is running them as a single habit every time you create a file. Walk through the decision below. It’s the exact question order an experienced developer runs through when they add a new file to the project.

Where does this file go?

That walk is the entire placement model expressed as a question order. Learn the order, routable first, then feature count, then kind, and you’ll know where any new file belongs without having to think it through.

Files the framework reads at the top of app/

Section titled “Files the framework reads at the top of app/”

A page.tsx isn’t the only filename Next.js treats specially. There’s a small vocabulary of reserved names the framework looks for. Meeting them now keeps you from confusing them with ordinary files, or from accidentally giving one of your own files a name that triggers a framework behavior. Each gets a sentence here and a full lesson later.

route.ts, the other routable file. A segment becomes visitable with a page.tsx, but it can also become visitable with a route.ts instead. The difference is what they return. A page.tsx renders UI at a URL, while a route.ts returns raw HTTP responses at a URL: JSON, a redirect, a file, with no UI at all. It’s how you build an API endpoint inside the App Router, following the same folder-is-a-segment rule with a different leaf file. So app/api/health/route.ts serves /api/health as an endpoint. That’s all you need for now, since a later chapter covers route handlers in full. The line to keep: page.tsx is UI at a URL, route.ts is an HTTP endpoint at a URL.

Reserved files at app/’s root. A handful of filenames generate behavior simply by existing at the right path. globals.css is the global stylesheet the root layout imports. favicon.ico becomes the browser-tab icon with no configuration. And a set of metadata files (robots.ts, sitemap.ts, opengraph-image.png, icon.png, and their siblings) generate SEO and social-sharing artifacts just by being present. You don’t need to know any of them in depth yet, since a later chapter covers the SEO set. The takeaway is narrow: some filenames at app/ are framework-reserved, so don’t reuse those names for your own files.

The Pages Router, in one line. Before the App Router, Next.js routed through a pages/ directory with different rules. A legacy codebase may still have one; this course teaches only the App Router, and migrating off pages/ is out of scope. If you see a pages/ folder, read it as “the old system” and keep moving.

This whole lesson comes down to one question you should now be able to answer at a glance for any file: does it create a URL, or not? The sort below puts every rule from the lesson into one drill. A page.tsx and a route.ts make URLs; underscores, missing pages, and anything outside app/ don’t. Drop each item in the right bucket.

Sort each path by whether visiting it serves a URL. Drag each item into the bucket it belongs to, then press Check.

Creates a URL Visiting this path serves something.
Never a URL Reachable by import at most, never visited.
app/page.tsx
app/blog/page.tsx
app/api/health/route.ts
app/(marketing)/page.tsx
app/dashboard/_components/chart.tsx
app/dashboard/ (folder, no page.tsx)
src/lib/utils.ts
app/_lib/format.ts

The app/(marketing)/page.tsx item is a deliberate teaser. A folder wrapped in parentheses is a route group: it organizes files without contributing a segment to the URL, so that page.tsx still renders at /, not /(marketing). That’s the next lesson’s territory. For now, just file it under “creates a URL, the parentheses vanish from the path.”

The official Next.js documentation has a dedicated page on these conventions, the canonical reference for the folder, file, and co-location rules this lesson covered. Keep it bookmarked; it’s the page you’ll come back to when you’re unsure whether a filename is reserved. The free Next.js Learn course walks the same routing rules hands-on, and the App Router Playground lets you click through them live.