Skip to content
Chapter 29Lesson 1

The file system is the route table

How the Next.js App Router turns the folder tree under app/ into your URLs, and where every other file belongs.

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

In most frameworks you’d open a routing config and register the URL against a component. Next.js has no such file, 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 read the URLs off any app/ tree, create the files that serve a target URL, and apply the one rule that places every other file in the project.

Folders are URL segments; page.tsx makes them routes

Section titled “Folders are URL segments; page.tsx makes them routes”

Two rules carry the whole system.

The first: every folder under app/ is a route segment , one slash-separated piece of the URL. A folder named dashboard contributes /dashboard, and an invoices folder nested inside it adds 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, the file whose default-exported component renders at that URL. A folder without one is legal, but Next.js serves no page there: visit it directly and you get a 404. 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, and 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. There’s no registration step because the files are the registration.

The diagram below makes the mapping literal: the app/ tree on the left, the URL each page.tsx produces on the right. 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 tend to trip people up.

The export must be default. Next.js imports the default export of every page.tsx to find the component to render; 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’s convention files are the exception, mandating a default export: page.tsx, layout.tsx, route.ts, and a handful of others. In those files a default export is correct; anywhere else, it’s a code smell.

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

It’s 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 a Server Component fully is; here, just note that page.tsx starts on the server side of that line.

Practice the mapping in both directions.

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: product wants a page at /team/members. Which files do you create? Work it out before opening 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.

Almost every Next.js tutorial, repo, and screenshot was scaffolded by pnpm create next-app, the official project generator. You won’t run it in this course, which ships its own canonical starter, but you should recognize what it produces so its output looks familiar in the wild.

The starter makes the same choices the wizard offers (Biome, a src/ directory), so this is also the tree you’ll work 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

The starter puts your code under src/, which is why your tree is src/app/… rather than a bare app/… at the repo root. That’s the only structural difference: same routing rules, one extra folder at the top. There’s no bundler to choose either, since Turbopack is the default in Next.js 16.

You can now create routes. The bigger question, the one that shapes the whole codebase, is where the rest of the code goes, and the App Router has a strong opinion worth treating as this course’s first architectural principle.

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?

  • 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, all components together, all hooks together, all helpers together. You’ll recognize it from older codebases, and it isn’t wrong everywhere, but for code that belongs to one route it’s the wrong default. The second tab organizes by feature: everything the dashboard needs lives 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. 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, which is already feature-shaped: it’s the route tree, so co-locating makes your folders mirror your product.

The payoff is change, which is most of what you do to a codebase after the first week. By feature, you open one folder to understand the dashboard and delete one folder to remove it. By layer, every change means hunting through five directories for the relevant files, and a clean deletion means tracking them down 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 group a few charts under a charts/ folder, is /dashboard/charts now a broken half-page?

Two things resolve this, and the difference between them matters.

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

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

If bare files are already safe, why bother with the underscore? Three reasons, in ascending order of weight:

  • Readability. Grouping supporting files under _components/ and _lib/ keeps the route folder scannable: page.tsx and a couple of named folders, instead of a dozen loose files to sort by eye.
  • Convention. Other developers and tooling read _components/ instantly as “co-located, non-routable.” Following the shared convention is free legibility.
  • Future-proofing. This is the one that matters most. Next.js keeps adding reserved filenames. Name a component default.tsx and Next.js reads it as a parallel-route fallback, a framework convention you’ll meet later in this chapter, so your component quietly stops 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.

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

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

That tree bakes in a few conventions from the project’s code standards. Filenames are kebab-case (revenue-chart.tsx, format-cents.ts), with framework-mandated names like page.tsx and layout.tsx as the only exceptions. The _actions.ts file holds a route’s server actions ; for now, just file it as “the dashboard’s server-side mutations.”

One more standard: when you import from _lib/, import the specific file, such as _lib/format-cents. There is no index.ts re-exporting the whole 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, defeating the Server/Client split you’ll spend the next chapter learning. Import the file, not the folder.

Where shared code lives, and how to import it

Section titled “Where shared code lives, and how to import it”

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

One rule resolves every case:

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.

Here is 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)

This is orientation, not a syllabus: app/ is routes, components/ is shared UI, lib/ is shared logic, and the rest are labeled boxes you’ll open in later chapters.

One mechanical detail in those imports is worth pinning down, because you’ll type it hundreds of times: how a route reaches a shared helper.

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

Fragile. Three levels up just to reach lib/. Move this page one folder over and every ../ is 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, so @/lib/format-cents resolves the same way from anywhere in the project. The contract: @/ for everything that crosses a feature boundary, relative paths only within a feature, from the same folder or a sibling. Inside app/dashboard/, the page imports its own chart with ./_components/revenue-chart, because they are one feature and move together. The moment an import reaches out of the feature, to lib/, to components/, or to another route, it goes through @/.

This dovetails with the import-ordering rule from the code standards: external packages first, then @/ aliases, then relative imports, separated by blank lines. The alias isn’t just ergonomics; it’s the visual cue that a dependency lives outside your feature.

You have both halves of the decision: co-locate by feature, promote on the second use. The skill is running them together every time you create a file. Walk the decision below; it’s the question order to run through when you add a new file.

Where does this file go?

Learn the order, routable first, then feature count, then kind, and you’ll know where any new file belongs.

page.tsx isn’t the only filename Next.js treats specially; the framework looks for a small vocabulary of reserved names, each of which gets a full lesson later. Knowing them now keeps you from giving one of your own files a name that triggers framework behavior.

route.ts, the other routable file. A segment can become visitable with a route.ts instead of a page.tsx, following the same folder-is-a-segment rule with a different leaf file: app/api/health/route.ts serves /api/health. The difference is what they return: page.tsx is UI at a URL, route.ts is an HTTP endpoint that returns raw HTTP responses (JSON, a redirect, a file) with no UI.

Reserved files at app/’s root. A handful of filenames generate behavior simply by existing: globals.css is the global stylesheet the root layout imports, favicon.ico becomes the browser-tab icon, and the metadata files (robots.ts, sitemap.ts, opengraph-image.png, icon.png, and siblings) produce SEO and social-sharing artifacts. Don’t reuse these 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; this course teaches only the App Router. If you see a pages/ folder in a legacy codebase, read it as “the old system” and keep moving.

By now you can answer one question for any file at a glance: does it create a URL, or not? A page.tsx or a route.ts makes one; underscores, folders with no page, and anything outside app/ don’t.

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 points ahead to the next lesson. A folder wrapped in parentheses is a route group: it organizes files without adding a segment to the URL, so that page.tsx still renders at /, not /(marketing). For now, file it under “creates a URL”, with the parentheses gone 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.