| | |

Next.js 02 ▲ Project Structure — Folder conventions & file organization

⏱️ Estimated read: 13 min | |

The create-next-app scaffolds a project with a specific folder layout, but the layout is a convention, not a constraint. Next.js is unopinionated about how the non-routing files are organized; the app/ directory defines the routes, and everything else can be colocated wherever the developer finds it clearest . The previous chapter created the project; this chapter explains what the generated files do, which files are special, and how the folder structure maps to the URL paths.

The App Router uses the file-system routing: each folder inside app/ represents a URL segment, and a route becomes publicly accessible only when a page.tsx or a route.ts file is added to that folder . The layout.tsx wraps the page and the nested layouts, the loading.tsx provides the Suspense fallback, the error.tsx provides the error boundary, and the not-found.tsx provides the 404 UI . The parentheses in a folder name create a route group that does not affect the URL, and the underscore prefix creates a private folder that is excluded from the routing .

Key point: A folder inside app/ is not a route until it contains a page.tsx or a route.ts file. The files that are not the special routing files can be safely colocated inside the route folders, because they are not routable and only the page.tsx or the route.ts output is sent to the client . The src/ directory is an optional convention that separates the application code from the configuration files, and the public/ directory remains in the project root regardless .


Why the project structure requires understanding the conventions

The routing problem.

The folder hierarchy in app/ maps directly to the URL hierarchy. The app/page.tsx is the / route, the app/blog/page.tsx is the /blog route, and the app/blog/authors/page.tsx is the /blog/authors route . The folder is the URL segment, and the page.tsx is the file that makes the segment publicly accessible. A folder without a page.tsx is a container for the nested routes, not a route itself.

The special-file problem.

The layout.tsx, the loading.tsx, the error.tsx, the not-found.tsx, and the route.ts are the special files that the framework recognizes by the name. The layout.tsx wraps the child routes and preserves the state across the navigations, the loading.tsx shows the instant loading UI, and the error.tsx catches the errors in the route segment . The name is the convention; a file named Layout.tsx or layout.ts (without the x) is not recognized as the special file.

The colocation problem.

The app/ directory allows the colocation of the components, the styles, and the tests inside the route folders, because only the page.tsx and the route.ts are routable . The private folders (the _folder prefix) are the explicit mechanism for the non-routable files, and the route groups (the (group) parentheses) are the mechanism for organizing the routes without affecting the URL . The colocation keeps the related files together, which is the practical benefit of the file-system routing.


a. The top-level files and folders

The project root contains the app/ directory, the public/ directory, and the configuration files.

my-app/
├── app/              # App Router: routes, layouts, pages
├── public/           # Static assets (images, fonts)
├── src/              # Optional: application source
├── next.config.ts    # Next.js configuration
├── package.json      # Dependencies and scripts
├── tsconfig.json     # TypeScript configuration
├── eslint.config.mjs # ESLint configuration
└── .env.local        # Local environment variables

The app/ is the App Router directory, and the public/ is the static assets that are served from the base URL . The src/ is optional, and when it is used, the app/ moves to src/app/ and the public/ stays in the root . The next.config.ts is the framework configuration, and the package.json is the dependencies and the scripts.

The src/ directory is the common convention for separating the application code from the configuration files.

my-app/
├── src/
│   ├── app/          # The App Router
│   ├── components/   # Reusable UI components
│   ├── lib/          # Utilities and API clients
│   └── hooks/        # Custom React hooks
├── public/
└── next.config.ts

The src/app/ is the App Router, and the src/components/, the src/lib/, and the src/hooks/ are the application folders that the developer creates . The public/ and the configuration files remain in the root. The tsconfig.json paths must be updated to include the src/ prefix when the @/* alias is used .


b. The routing files

The page.tsx is the file that makes the route publicly accessible, and the layout.tsx is the file that wraps the route and the nested routes.

app/
├── layout.tsx        # Root layout (required, wraps everything)
├── page.tsx          # The / route
├── about/
│   └── page.tsx      # The /about route
├── blog/
│   ├── layout.tsx    # Wraps /blog and its descendants
│   ├── page.tsx      # The /blog route
│   └── [slug]/
│       └── page.tsx  # The /blog/:slug dynamic route
└── api/
    └── users/
        └── route.ts  # The /api/users API endpoint

The app/layout.tsx is the root layout, and it is required; it must contain the <html> and the <body> tags . The app/page.tsx is the home page, and the app/about/page.tsx is the /about route. The app/blog/layout.tsx wraps the /blog and its descendants, and the app/blog/[slug]/page.tsx is the dynamic route that matches /blog/any-slug . The app/api/users/route.ts is the API endpoint that handles the requests to /api/users .

The loading.tsx, the error.tsx, and the not-found.tsx are the special files that the framework uses for the loading, the error, and the 404 states.

app/
├── loading.tsx       # Loading UI for the root
├── error.tsx         # Error UI for the root ('use client')
├── not-found.tsx     # 404 UI for the root
└── dashboard/
    ├── loading.tsx   # Loading UI for /dashboard
    ├── error.tsx     # Error UI for /dashboard
    └── page.tsx      # /dashboard

The loading.tsx automatically wraps the page in the Suspense boundary and shows the fallback while the page is loading . The error.tsx must be a Client Component ('use client'), and it receives the error and the reset props . The not-found.tsx is the 404 UI for the segment.


c. The route groups and the private folders

The route groups are the folders in the parentheses, and they do not affect the URL. The private folders are the folders with the underscore prefix, and they are excluded from the routing.

app/
├── (marketing)/
│   ├── layout.tsx    # Layout for the marketing pages
│   ├── about/
│   │   └── page.tsx  # The /about route
│   └── contact/
│       └── page.tsx  # The /contact route
├── (shop)/
│   ├── layout.tsx    # Layout for the shop pages
│   ├── cart/
│   │   └── page.tsx  # The /cart route
│   └── products/
│       └── page.tsx  # The /products route
└── blog/
    ├── _components/  # Private folder, not routable
    │   └── Post.tsx
    └── page.tsx      # The /blog route

The (marketing) and the (shop) are the route groups; the URLs are /about and /contact for the marketing pages, and /cart and /products for the shop pages, without the (marketing) or the (shop) segment . The _components is the private folder; the files inside it are not routable, and the folder is the safe place for the UI utilities and the data helpers . The route groups are the mechanism for the different layouts for the different sections, and the private folders are the mechanism for the colocation of the non-routable files.


Complete Example Session

# ============================================
# PART 1: THE ROOT LAYOUT
# ============================================
# app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

# ============================================
# PART 2: THE HOME PAGE
# ============================================
# app/page.tsx
export default function Page() {
  return <h1>Home</h1>
}

# ============================================
# PART 3: THE NESTED ROUTE
# ============================================
# app/about/page.tsx
export default function AboutPage() {
  return <h1>About</h1>
}

# ============================================
# PART 4: THE NESTED LAYOUT
# ============================================
# app/blog/layout.tsx
export default function BlogLayout({ children }: { children: React.ReactNode }) {
  return <section><nav>Blog</nav>{children}</section>
}

# ============================================
# PART 5: THE DYNAMIC ROUTE
# ============================================
# app/blog/[slug]/page.tsx
export default function PostPage({ params }: { params: { slug: string } }) {
  return <h1>Post: {params.slug}</h1>
}

# ============================================
# PART 6: THE API ROUTE
# ============================================
# app/api/users/route.ts
export async function GET() {
  return Response.json({ users: [] })
}

# ============================================
# PART 7: THE ROUTE GROUP
# ============================================
mkdir -p app/\(marketing\)/about
# app/(marketing)/about/page.tsx → the /about route

# ============================================
# PART 8: THE PRIVATE FOLDER
# ============================================
mkdir -p app/blog/_components
# app/blog/_components/Post.tsx → not routable

# ============================================
# PART 9: THE LOADING UI
# ============================================
# app/blog/loading.tsx
export default function Loading() {
  return <p>Loading...</p>
}

# ============================================
# PART 10: THE ERROR UI
# ============================================
# app/blog/error.tsx
'use client'
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return <div><p>{error.message}</p><button onClick={reset}>Retry</button></div>
}

The ten parts demonstrate the project structure: the root layout, the home page, the nested route, the nested layout, the dynamic route, the API route, the route group, the private folder, the loading UI, and the error UI. The critical distinction is between Parts 7 and 8: the route group does not affect the URL, and the private folder is not routable at all.


Quick Reference

Top-Level Folders

FolderPurpose
app/App Router: routes, layouts, pages
public/Static assets
src/Optional application source

Routing Files

FilePurpose
layout.tsxShared UI, wraps children (required at root)
page.tsxThe route UI (makes the route public)
loading.tsxLoading UI (Suspense boundary)
error.tsxError UI (Error boundary, 'use client')
not-found.tsx404 UI
route.tsAPI endpoint
template.tsxRe-rendered layout

URL Patterns

PathURL
app/page.tsx/
app/about/page.tsx/about
app/blog/[slug]/page.tsx/blog/:slug
app/(group)/page.tsx/ (group omitted)
app/blog/_components/Not routable

Naming Conventions

TypeConventionExample
Folderskebab-caseuser-profile/
ComponentsPascalCaseButton.tsx
UtilitiescamelCaseformatDate.ts
Hooksuse prefixuseAuth.ts

Best Practices

✅ Do This:

# Use the App Router for the new projects
app/                                                           # ✅

# Colocate the components inside the route folders
app/blog/_components/Post.tsx                                 # ✅

# Use the route groups for the different layouts
app/(marketing)/layout.tsx                                     # ✅

# Use the private folders for the non-routable files
app/blog/_lib/data.ts                                          # ✅

# Use the src/ directory for the separation
src/app/                                                       # ✅

# Keep the public/ in the root even with src/
public/                                                        # ✅

❌ Don’t Do This:

# Assume every folder in app/ is a route
app/blog/    # Not a route without a page.tsx                  # ❌

# Forget the root layout
# The layout.tsx is required                                    # ❌

# Use the error.tsx as a Server Component
# It must be a Client Component ('use client')                  # ❌

# Put the public/ inside src/
# It must remain in the root                                    # ❌

# Use the route group for the URL segment
app/(marketing)/about/    # The URL is /about, not /(marketing)/about # ❌

Common Pitfalls

PitfallWhy It HappensFix
The route is not accessibleNo page.tsxAdd the page.tsx
The layout is not appliedWrong folderPlace the layout.tsx in the parent folder
The error boundary does not workNo 'use client'Add the directive
The static asset is not servedNot in public/Move it to public/
The src/ alias is brokentsconfig paths not updatedAdd the src/ prefix

Real-World Examples

1. Root layout

// app/layout.tsx
export default function RootLayout({ children }) {
  return <html><body>{children}</body></html>
}

2. Home page

// app/page.tsx
export default function Page() { return <h1>Home</h1> }

3. Nested route

// app/about/page.tsx
export default function About() { return <h1>About</h1> }

4. Dynamic route

// app/blog/[slug]/page.tsx
export default function Post({ params }) { return <h1>{params.slug}</h1> }

5. API route

// app/api/users/route.ts
export async function GET() { return Response.json([]) }

6. Route group

app/(marketing)/about/page.tsx    # /about

7. Private folder

app/blog/_components/Post.tsx     # not routable

8. Loading UI

// app/blog/loading.tsx
export default function Loading() { return <p>Loading...</p> }

9. Error UI

// app/blog/error.tsx
'use client'
export default function Error({ error, reset }) { return <button onClick={reset}>Retry</button> }

10. Src directory

src/app/page.tsx    # the / route

Visual

The App Router Mapping

┌──────────────────────────────────────────────────────────────┐
│  THE FOLDER HIERARCHY MAPS TO THE URL HIERARCHY              │
│                                                              │
│  app/                                                        │
│  ├── page.tsx          → /                                   │
│  ├── about/                                                  │
│  │   └── page.tsx      → /about                              │
│  ├── blog/                                                   │
│  │   ├── page.tsx      → /blog                               │
│  │   └── [slug]/                                             │
│  │       └── page.tsx  → /blog/:slug                         │
│  └── api/                                                    │
│      └── users/                                              │
│          └── route.ts  → /api/users                          │
└──────────────────────────────────────────────────────────────┘

The Special Files

┌──────────────────────────────────────────────────────────────┐
│  THE SPECIAL FILES ARE RECOGNIZED BY THE NAME                │
│                                                              │
│  layout.tsx      → wraps the children, preserves the state    │
│  page.tsx        → the route UI, makes the route public       │
│  loading.tsx     → the Suspense fallback                      │
│  error.tsx       → the error boundary ('use client')          │
│  not-found.tsx   → the 404 UI                                 │
│  route.ts        → the API endpoint                           │
│                                                              │
│  The file name is the convention; the wrong name is ignored. │
└──────────────────────────────────────────────────────────────┘

The Route Group and the Private Folder

┌──────────────────────────────────────────────────────────────┐
│  THE GROUP DOES NOT AFFECT THE URL; THE PRIVATE IS NOT ROUTABLE│
│                                                              │
│  app/                                                        │
│  ├── (marketing)/                                            │
│  │   ├── layout.tsx                                          │
│  │   └── about/page.tsx    → /about (the group is omitted)   │
│  └── blog/                                                   │
│      ├── _components/      → not routable                    │
│      └── page.tsx          → /blog                           │
│                                                              │
│  The group is for the layouts; the private is for the files. │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
App Routerapp/ directory
Route filepage.tsx
Layout filelayout.tsx (required at root)
Loading fileloading.tsx (Suspense)
Error fileerror.tsx (Error boundary, 'use client')
API fileroute.ts
Route group(group) (no URL impact)
Private folder_folder (not routable)
Dynamic route[slug]
Static assetspublic/
Optional sourcesrc/
Root layout<html> and <body> required

Key takeaways:

  • The folder hierarchy in app/ maps to the URL hierarchy. The app/page.tsx is the / route, the app/blog/page.tsx is the /blog route, and the app/blog/[slug]/page.tsx is the /blog/:slug dynamic route . A folder without a page.tsx or a route.ts is not a route.
  • The special files are recognized by the name. The layout.tsx, the loading.tsx, the error.tsx, the not-found.tsx, and the route.ts are the conventions that the framework uses. The error.tsx must be a Client Component ('use client') .
  • The route groups do not affect the URL. The (marketing) folder groups the routes without adding the (marketing) segment to the URL. The group is the mechanism for the different layouts for the different sections .
  • The private folders are not routable. The _components and the _lib folders are excluded from the routing, which makes them the safe place for the colocated components and utilities .
  • The src/ directory is optional. When it is used, the app/ moves to src/app/, and the public/ and the configuration files remain in the root. The tsconfig paths must be updated to include the src/ prefix .
  • The colocation is safe. The files that are not the special routing files can be placed inside the route folders without becoming routable, because only the page.tsx and the route.ts output is sent to the client .

Remember: The Next.js project structure is the app/ directory with the special files, the route groups, the private folders, and the dynamic segments. The folder is the URL segment, and the page.tsx is the file that makes the route public. The layout.tsx wraps the children, the loading.tsx shows the fallback, and the error.tsx catches the errors. The route group does not affect the URL, and the private folder is not routable. The src/ is optional, and the public/ stays in the root. The result is a structure where the file system is the router, and the folder names are the URL paths.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!