Next.js Interview Prep
Next.js Fundamentals

Project Structure & Conventions

How Next.js Maps Files to Your Entire Application

LinkedIn Hook

"Where does this file go?"

I asked a senior developer this about a Next.js project. He said: "The file path IS the route. If you understand the folder structure, you understand the entire app."

That's the thing about Next.js — there's no router config file. No route registration. No switch statement mapping URLs to components. The app/ directory IS the router, and every file inside it has a specific meaning.

page.js = a route. layout.js = a wrapper. loading.js = a skeleton. error.js = a fallback. not-found.js = a 404. Miss one of these, and your app breaks in ways you won't see until production.

In Lesson 1.3, I break down every special file in the App Router, how route groups let you organize without affecting URLs, why private folders exist, and the exact rules that turn your folder tree into a working web application.

If an interviewer asks "how does Next.js routing work?" and you can't draw the folder structure on a whiteboard — you're not ready.

Read the full lesson → [link]

#NextJS #React #WebDevelopment #InterviewPrep #Frontend #AppRouter #JavaScript #100DaysOfCode


Project Structure & Conventions thumbnail


What You'll Learn

  • How the app/ directory maps folders to URL routes automatically
  • What every special file does: page.js, layout.js, loading.js, error.js, not-found.js, template.js
  • Route groups with parentheses (groupName) and why they don't affect URLs
  • Private folders with _prefix to exclude code from routing
  • The src/ convention and when to use it
  • How to mentally translate a folder tree into a URL structure

The Blueprint Analogy — Your Folder Structure IS the Architecture

Think of It Like a Building Blueprint

Imagine you are an architect. You don't write a separate document listing every room and its location — the blueprint itself IS the map. The hallways define how you navigate between rooms. The rooms define what exists at each location.

Next.js works identically. Your app/ directory is the blueprint. Each folder is a hallway (a URL segment). Each page.js file is a room (a rendered route). Each layout.js is the building's structural framing that wraps every room on that floor.

There is no separate "routing configuration" file. The folder tree IS the configuration. Move a folder, and the URL changes. Delete a page.js, and the route disappears. This is what "file-system based routing" means — and it is the single most important architectural concept in Next.js.


The app/ Directory — Command Center of Your Application

In Next.js 13+ with the App Router, everything starts inside the app/ directory. This is where routing, layouts, loading states, error handling, and page rendering all converge.

Here is a basic project structure:

my-app/
├── app/
│   ├── layout.js          # Root layout (required)
│   ├── page.js            # Home page → /
│   ├── loading.js         # Loading state for /
│   ├── error.js           # Error boundary for /
│   ├── not-found.js       # 404 page for /
│   ├── about/
│   │   └── page.js        # About page → /about
│   ├── blog/
│   │   ├── layout.js      # Blog layout (wraps all blog pages)
│   │   ├── page.js        # Blog index → /blog
│   │   └── [slug]/
│   │       └── page.js    # Blog post → /blog/my-post
│   └── dashboard/
│       ├── layout.js      # Dashboard layout
│       ├── page.js        # Dashboard home → /dashboard
│       ├── settings/
│       │   └── page.js    # Settings → /dashboard/settings
│       └── analytics/
│           └── page.js    # Analytics → /dashboard/analytics
├── public/
│   └── images/
├── next.config.js
└── package.json

The core rule: A folder becomes a URL segment. A page.js inside that folder makes it a routable page. Without page.js, the folder is just organizational structure — it creates a URL segment but returns a 404 if someone navigates to it.


Special Files — The Seven Files Next.js Recognizes

This is the part interviewers love. Next.js gives specific meaning to specific filenames. You cannot name them anything else — they must be exact.

1. page.js — The Route Itself

This is the only file that makes a route publicly accessible. No page.js = no route.

// app/about/page.js
// This file makes /about a visitable URL

export default function AboutPage() {
  return (
    <main>
      <h1>About Us</h1>
      <p>We build things with Next.js.</p>
    </main>
  );
}

Interview point: A folder without page.js is not a route. You can have app/dashboard/settings/ with a layout.js but no page.js — navigating to /dashboard/settings will 404 even though the folder exists.

2. layout.js — Persistent Wrapper

A layout wraps all pages at its level and below. It does NOT re-mount when you navigate between its child routes. This means state, scroll position, and fetched data persist across navigations.

// app/layout.js (Root layout — REQUIRED, must include <html> and <body>)

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <nav>Site Navigation</nav>
        {children}
        <footer>Site Footer</footer>
      </body>
    </html>
  );
}
// app/dashboard/layout.js (Nested layout — wraps all /dashboard/* pages)

export default function DashboardLayout({ children }) {
  return (
    <div className="dashboard-container">
      <aside>Dashboard Sidebar</aside>
      <main>{children}</main>
    </div>
  );
}

Critical interview detail: The root layout (app/layout.js) is mandatory. It must contain <html> and <body> tags. There is no way around this. If you delete it, your app will not build.

Why layouts persist: When you navigate from /dashboard/settings to /dashboard/analytics, the DashboardLayout does NOT unmount and remount. Only the {children} (the page content) swaps out. This is a massive performance win — the sidebar doesn't flicker, the state doesn't reset.

3. loading.js — Automatic Suspense Boundary

Drop a loading.js file into any route folder, and Next.js automatically wraps the page in a React <Suspense> boundary using your loading component as the fallback.

// app/dashboard/loading.js
// Shown while /dashboard/page.js is loading

export default function DashboardLoading() {
  return (
    <div className="skeleton">
      <div className="skeleton-header" />
      <div className="skeleton-content" />
      <div className="skeleton-sidebar" />
    </div>
  );
}

What happens under the hood: Next.js transforms this into:

<DashboardLayout>
  <Suspense fallback={<DashboardLoading />}>
    <DashboardPage />
  </Suspense>
</DashboardLayout>

You never write the <Suspense> wrapper yourself. The file convention does it for you. This is instant streaming — the layout renders immediately, and the page content streams in when ready.

4. error.js — Automatic Error Boundary

This file catches JavaScript errors in its route segment and renders a fallback UI instead of crashing the whole app.

// app/dashboard/error.js
// MUST be a Client Component — error boundaries need state

"use client";

export default function DashboardError({ error, reset }) {
  return (
    <div className="error-container">
      <h2>Something went wrong in Dashboard</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Try Again</button>
    </div>
  );
}

Interview must-know: error.js MUST have "use client" at the top. Error boundaries are a React Client Component feature — they use componentDidCatch internally, which requires state. If you forget "use client", it will not work.

The reset function: Calling reset() attempts to re-render the error boundary's contents. It does NOT do a full page refresh — it re-executes the server component that failed.

Scoping: An error.js in app/dashboard/ catches errors for the dashboard page and all its children. But it does NOT catch errors in app/dashboard/layout.js — the error boundary sits below the layout in the component tree. To catch layout errors, place error.js in the parent folder.

5. not-found.js — Custom 404 Page

This file renders when notFound() is called from a server component, or when Next.js cannot match a URL to any route.

// app/not-found.js (Global 404)

export default function NotFound() {
  return (
    <div>
      <h1>404 — Page Not Found</h1>
      <p>The page you are looking for does not exist.</p>
      <a href="/">Go Home</a>
    </div>
  );
}
// app/blog/[slug]/page.js
// Triggering not-found programmatically

import { notFound } from "next/navigation";

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);

  // If no post found, render the nearest not-found.js
  if (!post) {
    notFound();
  }

  return <article>{post.content}</article>;
}

Interview detail: You can place not-found.js at any level. app/not-found.js handles global 404s. app/blog/not-found.js handles blog-specific 404s. Next.js uses the nearest one up the tree.

6. template.js — Like Layout, But Re-mounts

A template is structurally identical to a layout, but it creates a NEW instance on every navigation. It re-mounts, re-runs effects, and resets state.

// app/dashboard/template.js
// Re-mounts on every navigation within /dashboard/*

export default function DashboardTemplate({ children }) {
  // useEffect would run on every navigation
  // State would reset on every navigation
  return (
    <div>
      <p>Dashboard Navigation Timestamp: {Date.now()}</p>
      {children}
    </div>
  );
}

When to use template over layout:

  • You need useEffect to run on every navigation (analytics tracking)
  • You need to reset form state when switching between pages
  • You need enter/exit animations per route

Interview comparison: Layout = persistent singleton. Template = fresh instance per navigation. Most of the time, you want layout.js. Use template.js only when re-mounting behavior is explicitly required.

7. global-error.js — Root-Level Error Boundary

This is the error boundary for the root layout itself. Since error.js sits below its sibling layout.js, a regular error.js in app/ cannot catch errors from app/layout.js. That is what global-error.js is for.

// app/global-error.js
// Catches errors in the ROOT layout
// Must include its own <html> and <body> because it replaces the root layout

"use client";

export default function GlobalError({ error, reset }) {
  return (
    <html>
      <body>
        <h1>Something went terribly wrong</h1>
        <button onClick={() => reset()}>Try Again</button>
      </body>
    </html>
  );
}

Interview catch: global-error.js must define its own <html> and <body> tags because when it activates, it replaces the root layout entirely. This file only triggers in production — in development, the error overlay shows instead.

Project Structure & Conventions visual 1


The Nesting Hierarchy — How Special Files Stack

Understanding the order is critical for interviews. When Next.js renders a route, it nests the special files in this exact order:

layout.js
  template.js
    error.js (ErrorBoundary)
      loading.js (Suspense)
        not-found.js (NotFoundBoundary)
          page.js

This means:

  • Layout wraps everything and persists across navigation
  • Template sits inside layout and re-mounts per navigation
  • Error boundary catches errors from loading, page, and not-found
  • Suspense (loading) wraps the page, showing a fallback while it loads
  • Page is the innermost component — the actual content

If you draw this on a whiteboard during an interview, you will impress.


Route Groups — Organize Without Affecting URLs

Route groups use parentheses (folderName) to organize files without adding a URL segment.

The Problem

You want to group your authentication pages (/login, /register, /forgot-password) in one folder for organizational reasons. But you do NOT want the URL to be /auth/login — you just want /login.

The Solution

app/
├── (auth)/
│   ├── layout.js          # Shared auth layout (centered card, no navbar)
│   ├── login/
│   │   └── page.js        # → /login (NOT /auth/login)
│   ├── register/
│   │   └── page.js        # → /register
│   └── forgot-password/
│       └── page.js        # → /forgot-password
├── (marketing)/
│   ├── layout.js          # Marketing layout (big hero, CTA buttons)
│   ├── page.js            # → / (home page)
│   ├── about/
│   │   └── page.js        # → /about
│   └── pricing/
│       └── page.js        # → /pricing
└── (dashboard)/
    ├── layout.js           # Dashboard layout (sidebar, auth required)
    ├── dashboard/
    │   └── page.js         # → /dashboard
    └── settings/
        └── page.js         # → /settings

Key points:

  • Parentheses () make the folder invisible to the URL
  • Each route group can have its own layout.js — this is the main reason they exist
  • You can have multiple root-level layouts by splitting your app into route groups
  • Route groups are purely organizational — they change nothing about how the app works at runtime

Multiple Root Layouts

One powerful pattern: removing the global app/layout.js and giving each route group its own root layout:

app/
├── (shop)/
│   ├── layout.js          # Shop layout: <html><body> with shop nav
│   └── products/
│       └── page.js
├── (admin)/
│   ├── layout.js          # Admin layout: <html><body> with admin nav
│   └── users/
│       └── page.js

When you do this, each route group's layout MUST include <html> and <body> tags since there is no shared root layout above them. Navigating between route groups causes a full page reload because the root layouts are different.


Private Folders — The _ Prefix

Any folder starting with an underscore is completely excluded from routing.

app/
├── _components/           # NOT a route — private folder
│   ├── Button.js
│   ├── Modal.js
│   └── Sidebar.js
├── _lib/                  # NOT a route — private folder
│   ├── database.js
│   └── auth.js
├── _utils/                # NOT a route — private folder
│   └── formatDate.js
├── dashboard/
│   ├── _components/       # Dashboard-specific private components
│   │   └── DashboardChart.js
│   └── page.js
└── page.js

Why use private folders:

  • Separate UI logic from routing logic
  • Colocate related files without accidentally creating routes
  • Consistently organize internal code across the project
  • Prevent someone from accidentally navigating to /components or /lib

Interview note: Private folders are a convention, not a hard requirement. You could also put shared code in a top-level lib/ or components/ folder outside of app/. But _prefix inside app/ keeps related code physically close to the routes that use it.


The src/ Convention

Next.js optionally supports placing app/ inside a src/ directory:

my-app/
├── src/
│   ├── app/               # Routing lives here
│   │   ├── layout.js
│   │   └── page.js
│   ├── components/        # Shared components
│   ├── lib/               # Utilities
│   └── styles/            # CSS
├── public/                # Static assets (stays at root)
├── next.config.js         # Config (stays at root)
└── package.json

Rules:

  • src/app/ replaces app/ — you cannot have both
  • public/ stays at the project root, NOT inside src/
  • Config files (next.config.js, tsconfig.json, .env) stay at the root
  • This is purely organizational preference — no performance or feature difference

When to use src/: When you want a clean separation between application code and config files. Most large teams prefer it. Smaller projects often skip it.


File Structure to URL — The Mental Map

Here is the complete translation table. Master this, and you can reverse-engineer any Next.js project:

FOLDER STRUCTURE                          URL PATH
─────────────────────────────────────────────────────────
app/page.js                            → /
app/about/page.js                      → /about
app/blog/page.js                       → /blog
app/blog/[slug]/page.js                → /blog/:slug
app/blog/[...slug]/page.js             → /blog/*  (catch-all)
app/blog/[[...slug]]/page.js           → /blog OR /blog/* (optional catch-all)
app/(auth)/login/page.js               → /login  (group ignored)
app/(auth)/register/page.js            → /register  (group ignored)
app/dashboard/settings/page.js         → /dashboard/settings
app/_utils/helpers.js                  → NOT a route (private)
app/api/users/route.js                 → /api/users (API route)

The rules summarized:

  1. Folders = URL segments
  2. page.js = makes the segment routable
  3. [brackets] = dynamic segment
  4. (parentheses) = invisible to URL
  5. _underscore = invisible to URL and routing
  6. route.js = API endpoint (no UI)

Project Structure & Conventions visual 2


Common Mistakes

  • Forgetting page.js and wondering why a route 404s. A folder alone is not a route. Without page.js, Next.js has nothing to render. This catches people who migrate from Pages Router where every file in pages/ is automatically a route.

  • Placing "use client" in error.js as an afterthought — or forgetting it entirely. error.js MUST be a Client Component. If you export a Server Component from error.js, the error boundary will not function. This is because React's error boundary mechanism (componentDidCatch) only exists in class components/client components.

  • Expecting error.js to catch errors in the same-level layout.js. It does not. The error boundary sits BELOW the layout in the component tree. If app/dashboard/layout.js throws, app/dashboard/error.js will NOT catch it — you need app/error.js (the parent level) to catch it. This nesting rule trips up even experienced developers.

  • Creating a route group that conflicts with a real route. If you have app/(marketing)/about/page.js and app/about/page.js, both resolve to /about. Next.js will throw a build error. Route groups must not create overlapping routes.


Interview Questions

Q: What is the difference between layout.js and template.js?

layout.js persists across navigations — it does not unmount and remount when the user navigates between sibling routes. State is preserved, effects do not re-run, and the DOM is not recreated. template.js creates a new instance on every navigation — state resets, effects re-run, the DOM is recreated. Use layout.js by default; use template.js only when you need fresh state or effects on each navigation (like page transition animations or analytics logging).

Q: Can a folder without page.js still affect routing?

Yes. A folder without page.js still creates a URL segment and can contain layout.js, loading.js, or error.js that wrap its child routes. But navigating directly to that folder's URL will return a 404 because there is nothing to render. The folder participates in the layout hierarchy without being a visitable route.

Q: How do route groups work, and why would you use them?

Route groups use parentheses (name) in the folder name. The parenthesized name is excluded from the URL path. You use them to organize routes logically (auth pages, marketing pages, dashboard pages) and to apply different layouts to different sections of your app — without adding extra URL segments. Each route group can have its own layout.js.

Q: Why must error.js be a Client Component?

React's error boundary mechanism relies on componentDidCatch and getDerivedStateFromError, which are class component lifecycle methods that only work on the client. Server Components cannot catch rendering errors the same way. Next.js requires "use client" in error.js so it can function as a proper React error boundary. The reset function it receives allows re-rendering the failed segment without a full page refresh.

Q: What happens if both app/(marketing)/about/page.js and app/about/page.js exist?

This creates a route conflict because both resolve to /about. Next.js will throw an error during the build process. Route groups are invisible to the URL, so they must not produce duplicate routes. You must ensure that only one page.js maps to each unique URL path across all route groups.


Quick Reference — Cheat Sheet

┌─────────────────────────────────────────────────────────────────┐
│              NEXT.JS APP ROUTER SPECIAL FILES                   │
├─────────────────┬───────────────────────────────────────────────┤
│  page.js        │  Route UI — makes a segment publicly visible  │
│  layout.js      │  Persistent wrapper — does NOT re-mount       │
│  template.js    │  Re-mounting wrapper — fresh on each nav      │
│  loading.js     │  Suspense fallback — auto loading skeleton    │
│  error.js       │  Error boundary — MUST be "use client"        │
│  not-found.js   │  404 UI — triggered by notFound() or no match │
│  global-error.js│  Root layout error catcher — needs html/body  │
│  route.js       │  API endpoint — GET/POST/PUT/DELETE handlers  │
├─────────────────┴───────────────────────────────────────────────┤
│  FOLDER CONVENTIONS                                             │
├─────────────────┬───────────────────────────────────────────────┤
│  folder/        │  URL segment                                  │
│  [folder]/      │  Dynamic segment  (/blog/:id)                 │
│  (folder)/      │  Route group — invisible to URL               │
│  _folder/       │  Private — excluded from routing entirely     │
│  src/app/       │  Optional — replaces app/ at root             │
├─────────────────┴───────────────────────────────────────────────┤
│  NESTING ORDER (outer → inner)                                  │
│  layout → template → error → loading → not-found → page        │
└─────────────────────────────────────────────────────────────────┘

Previous: Lesson 1.2 — App Router vs Pages Router → Next: Lesson 1.4 — Next.js Compilation & Bundling →


This is Lesson 1.3 of the Next.js Interview Prep Course — 8 chapters, 33 lessons.

On this page