Skip to content
beginner Phase 1 · Project Setup and File Conventions

App Directory Conventions

Understand which files create routes, layouts, and shared code.

45m
0 problems
Topic Progress 0%

Which Files Become URLs: page, route, and the Folders That Never Do

The App Router has one rule, and every routing question reduces to it: a folder becomes a URL only when it contains a route-producing special file — page or route. Everything else in app/ is either a modifier for a route that already exists, or ordinary code the router ignores. A fresh scaffold already demonstrates the rule:

app/
├── layout.tsx            # wraps everything below it — no URL of its own
├── page.tsx              # /
├── about/
│   └── page.tsx          # /about
├── blog/
│   ├── [slug]/
│   │   └── page.tsx      # /blog/hello-world, /blog/next-15, …
│   └── archive/
│       └── page.tsx      # /blog/archive
└── api/
    └── health/
        └── route.ts      # GET /api/health
Path URL it serves
app/page.tsx /
app/about/page.tsx /about
app/blog/[slug]/page.tsx any /blog/<one-segment>
app/docs/[...slug]/page.tsx /docs/a, /docs/a/b (required catch-all)
app/shop/[[...slug]]/page.tsx /shop as well as /shop/a/b
app/(marketing)/pricing/page.tsx /pricing — the group folder is invisible
app/_scratch/notes/page.tsx nothing — a private folder is excluded entirely

Two of those rows are where interviews go. Route groups (marketing) let you stack a different layout over a set of pages without adding a segment to the URL, so (marketing)/about and (dashboard)/about can both exist — which also means two groups accidentally declaring the same URL will collide at build time. Private folders _name are excluded from routing, which is how you keep experiment code inside app/ without publishing it.

The router matches names exactly: page.tsx, page.js, and page.jsx all register, but Page.tsx or pages.tsx do not — a case or plural slip produces a 404 with no warning in development on Windows or macOS, because those filesystems ignore case and Linux does not. The same exactness applies to the special files themselves; there is no partial credit in file-convention routing.

The counterpart is route.ts, which is an endpoint rather than a page:

// app/api/health/route.ts
export async function GET() {
  return Response.json({ ok: true, uptime: process.uptime() })
}

Export HTTP method functions (GET, POST, PATCH, DELETE) and the folder becomes an API route — the App Router's replacement for pages/api/*.ts. Two conventions to respect: it must be .ts/.js, not .tsx, because there is no JSX in it; and a segment serves one purpose, so choose page.tsx for UI or route.ts for an endpoint in a given folder rather than trying to host both.

Notice what does not produce a URL. A folder with no page or route file — app/components/, app/dashboard/widgets/ — renders nothing when visited. That is the biggest behavioral shift from the Pages Router, where every .tsx file in pages/ became a route and shipping pages/components/Button.tsx publicly exposed /components/Button as a live URL. In app/, you may colocate ProductCard.tsx, schema.ts, and helpers.test.ts next to the page that uses them; the router scans for special file names only, so the code stays route-private by default.

The failure mode to hold onto: a folder named for something internal (app/utils/, app/(old)/legacy/) silently stays harmless — until someone drops a page.tsx inside, and the URL appears in production with no configuration change, no route table entry to review, no warning. File location is the public API of your site, which is precisely why Scaffolding with create-next-app puts such weight on choosing app/ (or src/app/) deliberately: that directory is where every URL you will ever ship is declared.

layout, loading, error: Which Special File Runs Where

If page and route decide what is served, the remaining special files decide how a segment behaves while it is served. Each one has exactly one job, and each has a granularity that interviewers like to test.

File Job Scope
page.tsx UI for this segment's URL that URL only
route.ts HTTP endpoint (GET, POST, …) that URL only
layout.tsx Wrapper around children; persists across navigation segment and everything nested under it
template.tsx Like a layout, but remounts on every navigation segment and below
loading.tsx Instant fallback while the segment's data resolves one Suspense boundary
error.tsx Recoverable UI when rendering below it throws one error boundary
not-found.tsx 404 UI for notFound() or an unmatched URL at this segment segment and below
default.tsx Fallback when a parallel route (@slot) has no match that slot

Three details cause most bugs.

error.tsx must be a Client Component. It is an error boundary, and React error boundaries only exist in the browser tree, so the file starts with 'use client' and receives an error object plus a reset() function:

'use client'
export default function Error({
  error,
  reset,
}: { error: Error & { digest?: string }; reset: () => void }) {
  return (
    <div>
      <h2>Could not load this order</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}

The second subtlety: an error.tsx catches failures thrown below it — typically the page.tsx in its own folder — but it does not catch errors thrown by the layout.tsx sitting beside it. A crash in the same segment's layout bubbles up to the parent's boundary, which is why a broken root layout takes down every route in the app instead of showing your nicest fallback.

loading.tsx is a Suspense boundary, not a spinner service. It renders the moment a navigation starts, inside the parent's layout, while the child page streams:

/navigation        → /dashboard/orders
┌──────────────────────────────────┐   ← root layout: never re-renders
│ App bar                           │
│ ┌──────────────────────────────┐ │   ← loading.tsx for /dashboard/orders
│ │ ▓▓▓▓▓  ▓▓▓▓▓  ▓▓▓▓▓        │ │      appears instantly
│ └──────────────────────────────┘ │
└──────────────────────────────────┘

Because it lives under the parent layout, the shell stays visible and the user keeps their place — the mechanism behind "instant navigation" in every App Router demo. Drop it in and the whole view blanks until the slow await fetch resolves.

Layouts compose and persist. app/layout.tsx wraps app/dashboard/layout.tsx, which wraps the matched page; each receives its nested UI through a children prop. On client-side navigation, layouts above the matched segment are not re-rendered, so open sidebars, scroll position, and useState survive a route change — a page component, by contrast, unmounts and remounts every time. template.tsx exists for the rare case where you want the opposite: a wrapper that re-creates its subtree on each navigation, for example a component that replays an entrance animation.

Recognizing these names as behavior boundaries rather than routes is the interview payoff: the question "what does loading.tsx actually do?" is answered by naming Suspense, the parent layout, and streaming — not by saying "it shows a spinner". The next file, Environment Variables, moves from file conventions to configuration, but the segment model here is what every later topic builds on.

Where Shared Code Lives So It Never Becomes a URL by Accident

Once routes are covered, the open question is where everything else goes: buttons used by six pages, the database client, date helpers, generated types. Put shared code in the wrong place and you either create URLs you did not intend or bury route-private logic where nobody can find it.

The safe foundation is that only app/ is scanned for route files, and only page/route inside it register. So this layout cannot produce a single URL outside app/:

src/
├── app/                  # every URL in the product lives here
│   ├── layout.tsx
│   ├── page.tsx
│   └── orders/
│       ├── page.tsx      # /orders
│       ├── order-card.tsx     # colocated: private to /orders
│       └── status.ts          # colocated: private to /orders
├── components/           # shared UI — Button, Modal, DataTable
├── lib/                  # db.ts, stripe.ts, fetch-api.ts
├── hooks/                # use-orders.ts
└── types/                # shared types, generated API models

components/, lib/, hooks/, and types/ sit outside the router's scan, so importing them cannot change a URL. Resolve them through the alias the scaffold created — import { Button } from '@/components/Button' — rather than relative ../../../.. chains that break the moment a file moves.

Colocation inside app/ is equally safe, and it is the App Router's intended style: a helper imported only by app/orders/page.tsx belongs next to it, not in a global folder it will never be reused from. The maintainers' rule of thumb is one level of colocation — files for the current segment live in the segment's folder; promote to src/components/ the moment a second route imports them.

Three mistakes blur this line:

  1. Special file names used as ordinary components. A colocated app/dashboard/loading.tsx that you intended as a spinner component is registered as the segment's loading boundary, and error.tsx becomes an error boundary. Name such files skeleton.tsx or order-error.tsx when they are just components.

  2. Internal folders that later grow a page. app/admin-utils/ is inert today. The day someone adds app/admin-utils/page.tsx to "test something", /admin-utils is live — public, crawlable, and unauthenticated unless the page defends itself. Treat every new page.tsx as a deliberate publication.

  3. Holding shared state in a route folder. A cart context defined in app/cart/context.tsx and imported by app/checkout/ creates a dependency from an unrelated route into another segment's internals. Move it to src/lib/ or src/hooks/ so the import graph reflects reality.

A quick way to reason about any file: if this URL were added to the sitemap tomorrow, would this file be part of the reason? If yes, it belongs to a segment; if no, it belongs outside app/.

One file lives outside this whole discussion: middleware.ts sits at the project root (or at the root of src/), never inside app/, because it is not a route — it runs before matching on every request that qualifies. Similarly, a colocated helpers.test.ts or schema.ts is inert: only the exact special names carry meaning, and even then only the lowercase ones. That exactness is the convention to internalize before moving on to Environment Variables.

Interviewers ask this as "where would you put a component used by two layouts?" — a strong answer names both halves: route-private code is colocated with its segment, cross-cutting code moves to src/components or src/lib, and neither choice can create a route because only page.tsx and route.ts do that. From here the natural next question is how those segments get configured at all, which is exactly Environment Variables — the data clients you just parked in lib/ need a DATABASE_URL that must never reach the browser.

Quiz

1. A folder under app/ contains only `layout.tsx` and `loading.tsx`, with no page or route file. What happens when you visit its path?

Question 1 options

2. Which of these App Router files must begin with 'use client'?

Question 2 options

3. You need a Button used by four routes, and it must never be routable. Where does it go?

Question 3 options

4. What does adding `app/(marketing)/pricing/page.tsx` do to the site's URLs?

Question 4 options

Flashcards

Question

Which two file names produce routable URLs under app/?

Answer

page (renders the UI for a segment) and route (exports HTTP methods for an endpoint). Every other special file only adjusts behavior for routes that already exist, and folders containing neither never respond to a request.

Question

What is the difference between a route group `(name)` and a private folder `_name`?

Answer

(group) folders organize code and scope layouts but are invisible in the URL, so two groups can declare the same path and collide. _private folders are excluded from routing entirely, so nothing inside them is ever matched, even indirectly.

Question

Why must error.tsx be a client component, and what does it fail to catch?

Answer

React error boundaries exist only in the client tree, so the file needs 'use client' and receives `error` plus `reset()`. It catches failures thrown below it (the page) but not errors thrown by the layout.tsx beside it in the same segment — those bubble to the parent's boundary.

Question

How does layout.tsx behave across client-side navigation compared with template.tsx?

Answer

Layouts are preserved when navigating below them, so component state such as an open sidebar or scroll position survives, and they receive nested output via `children`. Templates remount on every navigation, re-creating the subtree each time — useful for replaying entrance animations.

Question

Where does shared code live, and what promotes a file out of a route folder?

Answer

In src/components, src/lib, src/hooks, src/types — outside app/ they can never match a route. Colocate route-private files with their segment, and hoist to a shared folder the moment a second route imports them.

Revision Notes

Key Takeaways

  • 1. Routing has exactly two producers — page.tsx and route.ts — and folders without them do not respond, which is why colocation inside app/ is safe by default
  • 2. Route groups `(x)` vanish from URLs while private folders `_x` vanish from routing; both are how you organize without publishing
  • 3. loading.tsx is a Suspense boundary rendered inside the parent layout, and error.tsx is a client-side boundary that does not protect its own segment's layout
  • 4. Layouts persist across navigation (state survives) while page components unmount and remount — the mechanism interviewers probe with state-preservation questions
  • 5. Never give an ordinary component a special file name, and treat every new page.tsx as publishing a public URL with no review step

Interview Tips

  • • "How does the App Router decide what is routable?" — folders become segments, page/route register the URL, special files modify behavior, and everything else in app/ is ignored; state it as a rule and then show you know the exceptions (groups, private folders)
  • • "Where do you put shared components?" — outside app/ in src/components, imported via the @ alias, with the promotion rule: colocate until a second route imports it, then hoist
  • • "What does loading.tsx actually do?" — it becomes a Suspense boundary for the segment, streams instantly under the parent layout during navigation, and keeps the shell visible while data resolves
  • • "Why can't a layout be a client component?" — it can, but everything below it then ships JavaScript; the default is a server layout with a 'use client' island for the interactive parts, which keeps the payload small

Cheat Sheet

App Directory Conventions

What creates a URL (the only two):

File Result
page.tsx UI route for that segment (app/about/page.tsx → /about)
route.ts HTTP endpoint exporting GET/POST/… (app/api/health/route.ts)

What does not: any other folder in app/, files without special names, and everything outside app/ (src/components, src/lib, src/hooks). route.ts must be .ts/.js, never .tsx.

URL-shaping folders:

Folder Effect on URL
(group) route group layout scoping only — segment is invisible
_private private folder excluded from routing entirely
[slug] / [...slug] / [[...slug]] dynamic / catch-all / optional catch-all

Behavior modifiers (no URL of their own):

  • layout.tsx — wraps children, persists across navigation (state survives)
  • template.tsx — like a layout but remounts every navigation
  • loading.tsx — Suspense boundary, renders inside the parent layout while the segment streams
  • error.tsx — error boundary; must start with 'use client'; does not catch its own segment's layout
  • not-found.tsx — notFound() and unmatched URLs
  • default.tsx — fallback for a parallel route @slot with no match

Shared code placement:

  • Colocate route-private files (order-card.tsx, status.ts) in the segment folder.
  • Promote to src/components / src/lib when a second route imports them.
  • Never name a plain component loading.tsx/error.tsx — the router registers it.

Interview one-liners:

  • "How does routing decide?" → folder + page/route; no route config file exists.
  • "Why doesn't app/components/ create a route?" → only special files register, and it has none.
  • "What happens if I add page.tsx to an internal folder?" → the URL goes live immediately, with no other change.

Continue Learning