Dev Logs
/Next.js/ Chapter 2: Project Structure & Configuration
Chapters
  • 01Chapter 1: Introduction & Setup
  • 02Chapter 2: Project Structure & Configuration
    • Plain English Explanation
    • The reserved filenames
    • The rule that trips everyone up
    • How segments nest
    • Organizing without creating routes
    • . Colocation — just put files in the folder
    • . Private folders — prefix with _
    • . Route groups — wrap in (parentheses)
    • . src/ — move the whole app down one level
    • A structure that scales
    • public/ — static assets
    • next.config.ts
    • Type-safe routes
    • Remote images
    • Cache Components (Partial Prerendering)
    • React Compiler
    • Redirects and rewrites
    • Security headers
    • Deployment output
    • Turbopack
    • Bundling controls
    • Options that no longer exist
    • Environment variables
    • The one rule
    • Reading env vars at runtime
    • next typegen and the props helpers
    • The import alias
    • Common Pitfalls
    • . A folder with no page file, wondering why the route 404s
    • . page.tsx and route.ts in the same folder
    • . Expecting a secret in a Client Component
    • . Committing .env.local
    • . Assuming _components is required
    • . Editing next.config.ts and expecting hot reload
    • When & Why to Use Each Organization Tool
    • Mini Practice Problems
    • Problem 1: Which of these are routes?
    • Problem 2: Fix the config
    • Problem 3: Env var safety audit
    • Problem 4: Design the tree
    • Interview Notes
    • Common Questions
    • Asked at Companies
    • Visual Memory Aid
    • Key Takeaways
  • 03Chapter 3: Layouts & Pages
  • 04Chapter 4: Linking & Navigation
  • 05Chapter 5: Dynamic Routes & Params
  • 06Chapter 6: Route Groups & Organization
  • 07Chapter 7: Parallel & Intercepting Routes
  • 08Chapter 8: Loading, Suspense & Streaming
  • 09Chapter 9: Error Handling
  • 10Chapter 10: Server & Client Components
  • 11Chapter 11: Data Fetching
  • 12Chapter 12: Server Actions & Mutations
  • 13Chapter 13: Route Handlers
  • 14Chapter 14: Caching & use cache
  • 15Chapter 15: Revalidation & ISR
  • 16Chapter 16: Cache Components & Partial Prerendering
  • 17Chapter 17: Proxy (formerly Middleware)
  • 18Chapter 18: Authentication & Authorization
  • 19Chapter 19: Metadata, SEO & OG Images
  • 20Chapter 20: Images & Fonts
  • 21Chapter 21: Styling
  • 22Chapter 22: Performance Optimization
  • 23Chapter 23: Testing
  • 24Chapter 24: Deployment & Self-Hosting
  • 25Chapter 25: Upgrading to Next.js 16
  • 26Chapter 26: Capstone Project
  • 27Chapter 27: React Performance Profiling
All chapters

📁 Chapter 2: Project Structure & Configuration

Every special filename Next.js reserves, how to organize a real codebase without fighting the router, and the config options that actually matter.

📖 Plain English Explanation

Most frameworks ask you to register your routes somewhere. Next.js reads your folders instead.

That trade is powerful but has a catch: certain filenames are reserved. A file called page.tsx is not just a file — it is a route. A file called loading.tsx is a loading state. If you name a helper file route.ts by accident, Next.js will try to serve it as an API endpoint.

So the first thing to learn is the reserved vocabulary. There are about fifteen words. Once you know them, every Next.js codebase in the world looks familiar.

The second thing to learn is how to put your own files — components, utilities, types — next to your routes without accidentally creating URLs. Next.js has explicit tools for that, and this chapter covers all of them.

🗂️ The reserved filenames

Inside app/, these names have meaning. Everything else is just a file.

FileWhat it does
layoutShared UI that wraps a segment and all its children. Persists across navigation.
pageThe UI for a route. A folder is only a route if it has a page.
loadingInstant loading UI, backed by a Suspense boundary
not-foundUI for notFound() and unmatched URLs
errorError boundary for a segment
global-errorError boundary for the root layout itself
forbiddenUI for forbidden() — 403
unauthorizedUI for unauthorized() — 401
routeAn API endpoint (cannot coexist with page in the same folder)
templateLike layout, but remounts on every navigation
defaultFallback UI for a parallel route slot
proxyRuns before a request is completed (root-level only)
instrumentationServer startup hook — observability, monitoring
instrumentation-clientBrowser startup hook
mdx-componentsRequired to use MDX

Each accepts .js, .jsx, .ts, or .tsx.

Plus a set of metadata files, which are covered in Chapter 19:

favicon.ico          icon.png          apple-icon.png
opengraph-image.tsx  twitter-image.tsx
robots.txt           sitemap.ts        manifest.json

The rule that trips everyone up

A folder becomes a URL only when it contains a page file.

app/
├── page.tsx              →  /              ✅ route
├── dashboard/
│   ├── layout.tsx        →  (no route — just wraps children)
│   ├── page.tsx          →  /dashboard     ✅ route
│   └── settings/
│       └── page.tsx      →  /dashboard/settings   ✅ route
└── lib/
    └── utils.ts          →  (nothing — no page file)

app/lib/utils.ts is not a route. It's safe to put helper files inside app/.

🧩 How segments nest

Every folder is a route segment. Layouts nest automatically, parent wrapping child:

app/
├── layout.tsx                    ← root layout
├── page.tsx
└── shop/
    ├── layout.tsx                ← shop layout
    ├── page.tsx
    └── [category]/
        ├── layout.tsx            ← category layout
        └── page.tsx

Visiting /shop/shoes renders:

RootLayout
  └── ShopLayout
        └── CategoryLayout
              └── CategoryPage

Layouts do not re-render when you navigate between their children. Navigating from /shop/shoes to /shop/hats keeps ShopLayout mounted — scroll position, state, and running videos survive. This is the single biggest UX advantage of the App Router, and Chapter 3 goes deeper.

🔒 Organizing without creating routes

You have four tools. Use them deliberately.

1. Colocation — just put files in the folder

Any file without a reserved name is inert. This is the default and usually the right answer:

app/dashboard/
├── page.tsx
├── layout.tsx
├── DashboardChart.tsx      ← component, not a route
├── formatCurrency.ts       ← helper, not a route
└── types.ts                ← types, not a route

2. Private folders — prefix with _

A folder starting with an underscore is completely excluded from routing, including everything inside it:

app/
├── _components/            ← never a route
│   ├── Button.tsx
│   └── Card.tsx
├── _lib/
│   └── db.ts
└── dashboard/
    └── page.tsx            →  /dashboard

Use this when a folder would otherwise look like a route and confuse people.

To route a URL that literally starts with an underscore, use %5F (the URL-encoded underscore) in the folder name.

3. Route groups — wrap in (parentheses)

A folder in parentheses organizes files without adding a URL segment:

app/
├── (marketing)/
│   ├── layout.tsx          ← applies to about + pricing only
│   ├── about/page.tsx      →  /about       (NOT /marketing/about)
│   └── pricing/page.tsx    →  /pricing
└── (app)/
    ├── layout.tsx          ← a totally different shell
    ├── dashboard/page.tsx  →  /dashboard
    └── settings/page.tsx   →  /settings

This is how you give a marketing site and a logged-in app completely different layouts at the same URL level. Chapter 6 covers the patterns in full.

4. src/ — move the whole app down one level

If you prefer application code separated from config files:

my-app/
├── src/
│   ├── app/
│   └── components/
├── public/
├── next.config.ts
└── package.json

public/ and config files stay at the root. Everything else behaves identically. Purely a preference — pick one and be consistent.

🧱 A structure that scales

Small apps can colocate everything. Once you pass a dozen routes, most teams land on something like this:

my-app/
├── app/
│   ├── (marketing)/
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   └── pricing/page.tsx
│   ├── (dashboard)/
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   └── settings/page.tsx
│   ├── api/
│   │   └── webhooks/stripe/route.ts
│   ├── layout.tsx
│   ├── globals.css
│   └── proxy.ts
├── components/
│   ├── ui/                    ← generic, reusable (Button, Input)
│   └── features/              ← domain-specific (InvoiceTable)
├── lib/
│   ├── db.ts
│   ├── auth.ts
│   └── utils.ts
├── hooks/
│   └── use-media-query.ts
├── types/
│   └── index.ts
├── public/
│   └── logo.svg
├── next.config.ts
└── tsconfig.json

Two guidelines that hold up over time:

  1. Route-specific code goes next to the route. A component used by exactly one page belongs in that page's folder.
  2. Shared code moves up. The moment a second route imports it, promote it to components/ or lib/.

Resist building a components/ folder with 200 files in it on day one.

📤 public/ — static assets

Files in public/ are served from the root URL:

public/
├── logo.svg        →  /logo.svg
├── favicon.ico     →  /favicon.ico
└── docs/guide.pdf  →  /docs/guide.pdf
tsx
// app/page.tsx
import Image from 'next/image'

export default function Page() {
  return <Image src="/logo.svg" alt="Logo" width={120} height={40} />
}
jsx
// app/page.js
import Image from 'next/image'

export default function Page() {
  return <Image src="/logo.svg" alt="Logo" width={120} height={40} />
}

Note the path is /logo.svg, not /public/logo.svg. Files here are not processed, hashed, or optimized by the bundler — they're copied as-is.

🎛️ next.config.ts

Next.js 16 ships first-class TypeScript config support. Use it — the autocomplete alone is worth it.

ts
// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  // your options here
}

export default nextConfig
js
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // your options here
}

module.exports = nextConfig

There are well over a hundred options. Here are the ones you'll actually reach for, grouped by why you'd want them.

Type-safe routes

ts
// next.config.ts
const nextConfig: NextConfig = {
  typedRoutes: true,
}

Now <Link href="/dashbaord"> is a compile error, not a 404 discovered in production.

Remote images

ts
// next.config.ts
const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'images.unsplash.com' },
      { protocol: 'https', hostname: '**.mycdn.com', pathname: '/assets/**' },
    ],
  },
}

⚠️ Changed in Next.js 16

images.domains is deprecated. Use images.remotePatterns, which lets you constrain protocol, path, and port instead of trusting a whole hostname.

js
// ❌ deprecated
images: { domains: ['example.com'] }

// ✅ current
images: { remotePatterns: [{ protocol: 'https', hostname: 'example.com' }] }

Chapter 20 covers the other image defaults that changed in 16.

Cache Components (Partial Prerendering)

ts
// next.config.ts
const nextConfig: NextConfig = {
  cacheComponents: true,
}

⚠️ Changed in Next.js 16

experimental.ppr, experimental.dynamicIO, and experimental.useCache were all removed and replaced by the single top-level cacheComponents flag. This is not a rename — the underlying model changed. Chapter 16 covers it properly.

React Compiler

ts
// next.config.ts
const nextConfig: NextConfig = {
  reactCompiler: true,
}

Automatic memoization — no more hand-written useMemo/useCallback. Stable in Next.js 16, but off by default because it relies on Babel and slows builds. Requires npm install -D babel-plugin-react-compiler.

Redirects and rewrites

ts
// next.config.ts
const nextConfig: NextConfig = {
  async redirects() {
    return [
      { source: '/old-blog/:slug', destination: '/blog/:slug', permanent: true },
    ]
  },
  async rewrites() {
    return [
      { source: '/docs/:path*', destination: 'https://docs.example.com/:path*' },
    ]
  },
}

A redirect changes the URL in the browser. A rewrite does not — the user sees /docs/intro while the content comes from elsewhere.

Security headers

ts
// next.config.ts
const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'X-Frame-Options', value: 'DENY' },
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
        ],
      },
    ]
  },
}

Deployment output

ts
// next.config.ts
const nextConfig: NextConfig = {
  output: 'standalone',   // self-contained Node server, ideal for Docker
  // output: 'export',    // fully static HTML, no server at all
}

Turbopack

ts
// next.config.ts
const nextConfig: NextConfig = {
  turbopack: {
    resolveAlias: {
      '@ui': './components/ui',
    },
  },
}

⚠️ Changed in Next.js 16

experimental.turbopack graduated to a top-level turbopack key.

js
// ❌ Next.js 15
experimental: { turbopack: { /* ... */ } }

// ✅ Next.js 16
turbopack: { /* ... */ }

Bundling controls

ts
// next.config.ts
const nextConfig: NextConfig = {
  // Tree-shake big barrel-file libraries
  optimizePackageImports: ['lodash', 'date-fns', '@mui/icons-material'],

  // Keep these out of the bundle; require() them at runtime on the server
  serverExternalPackages: ['sharp', 'pdfkit'],

  // Compile packages that ship untranspiled source
  transpilePackages: ['@my-org/ui'],
}

Options that no longer exist

⚠️ Changed in Next.js 16

These were removed. If you see them in a config, it's from an older version:

RemovedReplacement
serverRuntimeConfigprocess.env.MY_VAR in Server Components
publicRuntimeConfigprocess.env.NEXT_PUBLIC_MY_VAR
eslint: {}run ESLint yourself (next lint is gone)
amp: {}AMP support removed entirely
experimental.dynamicIOcacheComponents: true
experimental.useCachecacheComponents: true
devIndicators.buildActivity(indicator still exists, options removed)

🔐 Environment variables

Next.js reads .env files automatically, in this precedence order:

.env.local          ← highest priority, gitignored, your secrets
.env.development    ← loaded during next dev
.env.production     ← loaded during next build / next start
.env                ← defaults, committed

The one rule

Variables are server-only unless prefixed with NEXT_PUBLIC_.

bash
# .env.local
DATABASE_URL="postgresql://localhost:5432/mydb"   # server only — never sent to browser
STRIPE_SECRET_KEY="sk_live_..."                   # server only
NEXT_PUBLIC_SITE_URL="https://example.com"        # inlined into browser bundles
tsx
// app/page.tsx — Server Component, has access to everything
import { db } from '@/lib/db'

export default async function Page() {
  const url = process.env.DATABASE_URL       // ✅ works
  const users = await db.query(url, '...')
  return <div>{users.length} users</div>
}
tsx
// app/components/Analytics.tsx — Client Component
'use client'

export function Analytics() {
  const site = process.env.NEXT_PUBLIC_SITE_URL   // ✅ works
  const secret = process.env.STRIPE_SECRET_KEY    // ❌ undefined — and that's the point
  return <script data-site={site} />
}

NEXT_PUBLIC_ values are inlined at build time, baked into the JavaScript bundle. Changing one requires a rebuild.

Reading env vars at runtime

If you self-host and want to change a variable without rebuilding, force the read to happen per-request with connection():

tsx
// app/page.tsx
import { connection } from 'next/server'

export default async function Page() {
  await connection()                        // opt out of static prerendering
  const config = process.env.RUNTIME_CONFIG // read at request time, not build time
  return <p>{config}</p>
}
jsx
// app/page.js
import { connection } from 'next/server'

export default async function Page() {
  await connection()
  const config = process.env.RUNTIME_CONFIG
  return <p>{config}</p>
}

🏷️ next typegen and the props helpers

Next.js generates global type helpers for route props. Run:

bash
npx next typegen

You get three globals, no import needed:

tsx
// app/blog/[slug]/page.tsx
export default async function Page(props: PageProps<'/blog/[slug]'>) {
  const { slug } = await props.params
  const { page } = await props.searchParams
  return <h1>{slug}</h1>
}
tsx
// app/blog/layout.tsx
export default async function Layout(props: LayoutProps<'/blog'>) {
  return <section>{props.children}</section>
}
ts
// app/api/posts/[id]/route.ts
export async function GET(request: Request, context: RouteContext<'/api/posts/[id]'>) {
  const { id } = await context.params
  return Response.json({ id })
}

The route string is checked against your actual folder structure, so PageProps<'/blog/[slugg]'> fails to compile. next dev and next build run typegen for you; you only run it manually in CI or after adding routes while the dev server is stopped.

📐 The import alias

create-next-app sets this up:

json
// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./*"]
    }
  }
}

Which turns this:

ts
import { Button } from '../../../components/ui/Button'

...into this:

ts
import { Button } from '@/components/ui/Button'

Use it everywhere. Relative imports across more than one level become unmaintainable the first time you move a folder.

⚠️ Common Pitfalls

1. A folder with no page file, wondering why the route 404s

app/dashboard/
└── layout.tsx        ← /dashboard returns 404

Fix: add page.tsx. A layout alone renders nothing.

2. page.tsx and route.ts in the same folder

app/api/users/
├── page.tsx     ← ❌ conflict
└── route.ts     ← ❌ conflict

A segment can serve a UI page or an API endpoint, never both. Fix: move one.

3. Expecting a secret in a Client Component

tsx
'use client'
console.log(process.env.API_SECRET)   // undefined

This is a security feature, not a bug. Secrets must never reach the browser. Fix: fetch the data in a Server Component and pass the result down as props, or call a Server Action.

4. Committing .env.local

bash
# .gitignore — verify this line exists
.env*

create-next-app adds it. If you migrated a project by hand, check.

5. Assuming _components is required

Private folders are a tool, not a convention you must follow. app/dashboard/Chart.tsx is already safe — a .tsx file with a non-reserved name is never a route. Use _ only when the folder name would be misleading.

6. Editing next.config.ts and expecting hot reload

Config changes require a full restart of next dev. Next.js will usually tell you, but if a change seems ignored, restart first before debugging.

🎯 When & Why to Use Each Organization Tool

Colocation       →  default; component used by exactly one route
Private (_)      →  a folder that would otherwise look like a route
Route groups ()  →  different layouts, or grouping without a URL segment
src/             →  team preference for separating app code from config
top-level dirs   →  anything imported by two or more routes

Decision flow when you don't know where a file goes:

Is it used by exactly one route?
├── Yes → put it in that route's folder
└── No  → Is it generic UI?
          ├── Yes → components/ui/
          └── No  → Is it logic/data?
                    ├── Yes → lib/
                    └── No  → components/features/

🏋️ Mini Practice Problems

Problem 1: Which of these are routes?

app/
├── page.tsx
├── utils.ts
├── (auth)/
│   ├── login/page.tsx
│   └── register/page.tsx
├── _internal/
│   └── admin/page.tsx
├── blog/
│   └── layout.tsx
└── api/
    └── health/route.ts

List every URL this app serves.

Problem 2: Fix the config

This config is written for Next.js 15. Bring it to 16:

js
// next.config.js
module.exports = {
  images: { domains: ['cdn.example.com'] },
  experimental: {
    ppr: true,
    turbopack: { resolveAlias: { '@ui': './ui' } },
  },
  eslint: { ignoreDuringBuilds: true },
  publicRuntimeConfig: { apiUrl: '/api' },
}

Problem 3: Env var safety audit

Which of these leak a secret to the browser?

tsx
// A
export default async function Page() {
  const key = process.env.STRIPE_SECRET
  return <div>Loaded</div>
}

// B
export default async function Page() {
  const key = process.env.STRIPE_SECRET
  return <ClientWidget apiKey={key} />
}

// C
'use client'
export function Widget() {
  return <div>{process.env.NEXT_PUBLIC_ANALYTICS_ID}</div>
}

Problem 4: Design the tree

Sketch the app/ folder for a site with:

  • A public marketing site (/, /pricing, /about) with one layout
  • A logged-in dashboard (/dashboard, /dashboard/billing) with a different layout, including a sidebar that must not remount when navigating between the two dashboard pages
  • A Stripe webhook endpoint

💼 Interview Notes

Common Questions

Q: How does Next.js decide what's a route? A folder inside app/ becomes a URL segment; the segment is servable only if the folder contains a page (UI) or route (API) file. Folders in (parentheses) add no segment. Folders starting with _ are skipped entirely.

Q: What's the difference between a route group and a private folder? A route group (name) still routes its children, it just doesn't contribute a URL segment — it exists to share a layout or organize. A private folder _name removes itself and everything under it from routing completely.

Q: Why can't you have page.tsx and route.ts in the same segment? They both claim the same URL. One returns HTML, the other returns a Response. Next.js can't pick, so it errors at build time.

Q: How do environment variables work in Next.js? Everything in .env is available on the server. Only NEXT_PUBLIC_-prefixed variables are inlined into client bundles at build time. To read a variable at request time instead of build time, call await connection() first to opt out of prerendering.

Q: What does typedRoutes do? It generates a union type of every valid route in your app and applies it to href on <Link> and to router.push(). Typos become compile errors instead of runtime 404s.

🏢 Asked at Companies

  • Vercel: "How would you structure an app with both a public site and an authenticated dashboard needing different shells?"
  • Airbnb: "Where do you draw the line between colocating a component and promoting it to a shared folder?"
  • Stripe: "A junior engineer put an API key in a Client Component. What happens, and how do you prevent it in review?"
  • Notion: "Explain how layout nesting affects what re-renders during navigation."

📊 Visual Memory Aid

              app/ FOLDER SEMANTICS

  folder/          →  adds "/folder" to the URL
  (folder)/        →  adds NOTHING to the URL   (route group)
  _folder/         →  removed from routing      (private)
  [folder]/        →  dynamic segment           /:folder
  [...folder]/     →  catch-all                 /a/b/c
  [[...folder]]/   →  optional catch-all        / or /a/b/c
  @folder/         →  parallel route slot
  (.)folder/       →  intercepting route


              FILE PRECEDENCE IN A SEGMENT

  layout    ──►  wraps everything below, persists
    └── template  ──►  wraps, but remounts each nav
          └── error      ──►  catches errors below
                └── loading   ──►  Suspense fallback
                      └── not-found
                            └── page  ◄── your actual UI


              ENV VAR VISIBILITY

  DATABASE_URL         │  server ✅   browser ❌
  NEXT_PUBLIC_API_URL  │  server ✅   browser ✅  (inlined at build)

🎯 Key Takeaways

  1. A folder is a route only if it contains page or route. Everything else in app/ is just a file you can safely colocate.
  2. Learn the fifteen reserved filenames. They are the entire vocabulary of App Router structure, and they're the same in every Next.js codebase.
  3. Use route groups () for layout boundaries, private folders _ for clarity. Colocation is the default — don't over-organize on day one.
  4. NEXT_PUBLIC_ is the only thing separating a secret from the browser. Everything else stays on the server, and that's enforced, not conventional.
  5. Several config options were removed in Next.js 16 — serverRuntimeConfig, eslint, amp, experimental.ppr/dynamicIO/useCache. If a config you copied has them, it's from an older version.

Next Chapter: Layouts & Pages →

Practice: Take the app you scaffolded in Chapter 1 and restructure it into two route groups — (marketing) and (app) — each with its own layout. Confirm that /about and /dashboard render with different shells while sharing the root layout.


PreviousChapter 1: Introduction & SetupNextChapter 3: Layouts & Pages

Open source, free forever. Built by iammhador.

Contribute on GitHub