DEV Community

Cover image for Next.js Folder Zen: Mastering the app/ Directory
Gavin Cettolo
Gavin Cettolo

Posted on

Next.js Folder Zen: Mastering the app/ Directory

If you’ve recently started using Next.js 13+, you’ve probably opened a project and thought:

“Why does this folder structure feel so… different?”

With the App Router, the app/ directory became the heart of every modern Next.js project. But for many developers, especially those coming from the old pages/ router, the folder structure can feel confusing at first.

Let’s fix that.

TL;DR

  • The app/ directory in Next.js introduces a file-system-based architecture that controls routing, layouts, and rendering behavior.
  • Special files like page.tsx, layout.tsx, loading.tsx, error.tsx, and not-found.tsx define how routes behave.
  • A clean folder strategy with route groups and dynamic routes makes large Next.js apps scalable and maintainable.

Table of Contents


Why the app/ Directory Matters

When Next.js introduced the App Router, it wasn’t just a new folder.

It was a shift in how we design React applications.

Instead of scattering logic across multiple layers, the app/ directory organizes your app around routes and UI segments.

Think of it like this:

URL → Folder → UI
Enter fullscreen mode Exit fullscreen mode

This approach makes large applications easier to reason about.

Instead of asking:

"Where is the component for this page?"

You simply look at the folder that matches the route.


The Core Concept: File-Based Routing

Routing in the app/ directory is simple.

Folders define routes.

Example:

app/
  page.tsx
  about/
    page.tsx
  dashboard/
    page.tsx
Enter fullscreen mode Exit fullscreen mode

This produces:

/           → page.tsx
/about      → about/page.tsx
/dashboard  → dashboard/page.tsx
Enter fullscreen mode Exit fullscreen mode

A basic page file looks like this:

export default function Page() {
  return <h1>Hello Next.js</h1>
}
Enter fullscreen mode Exit fullscreen mode

That’s it.

No router configuration required.


The Essential Files Inside app/

Next.js uses special filenames to control behavior.

Here are the ones you'll use most.

page.tsx

Defines a route UI.

export default function Page() {
  return <div>Dashboard</div>
}
Enter fullscreen mode Exit fullscreen mode

Every accessible route must contain a page.tsx.


layout.tsx

Layouts wrap multiple pages and persist during navigation.
Example:

app/
  layout.tsx
  dashboard/
    layout.tsx
    page.tsx
Enter fullscreen mode Exit fullscreen mode

Example layout:

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <main>
      <nav>Navigation</nav>
      {children}
    </main>
  )
}
Enter fullscreen mode Exit fullscreen mode

Layouts do not re-render during navigation, making them perfect for:

  • Navigation bars
  • Sidebars
  • Shared UI

Layouts: The Secret Weapon

One of the biggest advantages of the App Router is nested layouts.

Example structure:

app/
  layout.tsx
  dashboard/
    layout.tsx
    analytics/
      page.tsx
Enter fullscreen mode Exit fullscreen mode

Rendering flow:

Root Layout
   ↓
Dashboard Layout
   ↓
Analytics Page
Enter fullscreen mode Exit fullscreen mode

This allows you to build complex UI structures without prop-drilling or layout duplication.


Loading and Error States

Next.js lets you define route-level UI states.

loading.tsx

Displayed while a route loads.

export default function Loading() {
  return <p>Loading...</p>
}
Enter fullscreen mode Exit fullscreen mode

Useful for:

  • skeleton screens
  • streaming UI

error.tsx

Handles errors inside a route segment.

"use client" // Error boundaries must be Client Components

export default function Error({ error }: { error:Error }) {
  return <p>Something went wrong</p>
}
Enter fullscreen mode Exit fullscreen mode

This prevents the entire app from crashing.


Error Boundaries: One Important Limitation

The error.tsx file works as a React Error Boundary for a route segment.

But there's an important limitation many developers miss.

Error boundaries do not catch errors inside event handlers.

They only catch errors that happen during:

  • rendering
  • server component execution
  • data fetching

Example error boundary:

"use client"

export default function Error({
  error,
  reset,
}: {
  error:Error
  reset: () => void
}) {
  return (
    <div>
      <h2>Something went wrong.</h2>
      <button onClick={() => reset()}>
        Try again
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Errors thrown inside event handlers must still be handled manually.

Example:

"use client"

export default function Button() {
  const handleClick = () => {
    try {
      throw new Error("Boom");
    } catch (err) {
      console.error(err);
    }
  }
  return <button onClick={handleClick}>Click me</button>
}
Enter fullscreen mode Exit fullscreen mode

Think of error.tsx as a UI safety net, not a full error handling system.


Handling 404 Pages with not-found.tsx

Every production app needs a good 404 experience.

Next.js handles this with not-found.tsx.

Example:

export default function NotFound() {
  return (
    <div>
      <h2>Page not found</h2>
      <p>The content you're looking for doesn't exist.</p>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

To trigger it, use the notFound() function.

import {notFound} from "next/navigation"

if (!post) {
  notFound()
}
Enter fullscreen mode Exit fullscreen mode

You can define multiple not-found.tsx files at different levels.

Example:

app/
  not-found.tsx
  dashboard/
    not-found.tsx
Enter fullscreen mode Exit fullscreen mode

The closest one in the route tree wins.


Dynamic Routes

Dynamic routes use square brackets.

Example:

app/
  blog/
    [slug]/
      page.tsx
Enter fullscreen mode Exit fullscreen mode

URLs generated:

/blog/my-first-post
/blog/nextjs-routing
/blog/react-performance
Enter fullscreen mode Exit fullscreen mode

Example implementation:

export default function BlogPost({
  params
}: {
  params: { slug: string }
}) {
  return <h1>{params.slug}</h1>
}
Enter fullscreen mode Exit fullscreen mode

You can also combine it with static generation.


Multiple Dynamic Segments

Example:

app/
  shop/
    [category]/
      [product]/
        page.tsx
Enter fullscreen mode Exit fullscreen mode

Routes:

/shop/laptops/macbook-pro
/shop/phones/iphone-15
Enter fullscreen mode Exit fullscreen mode

Example code:

export default function ProductPage({
  params
}: {
  params: {
    category:string
    product:string
  }
}) {
  return (
    <div>
      <h1>{params.product}</h1>
      <p>Category: {params.category}</p>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Catch-All Routes

Sometimes you need flexible routes.

Next.js supports catch-all segments.

[...slug]
Enter fullscreen mode Exit fullscreen mode

Example:

app/
  docs/
    [...slug]/
      page.tsx
Enter fullscreen mode Exit fullscreen mode

Supported URLs:

/docs
/docs/getting-started
/docs/guides/routing
/docs/api/config
Enter fullscreen mode Exit fullscreen mode

Implementation:

export default function DocsPage({
  params
}: {
  params: { slug: string[] }
}) {
  return <div>{params.slug?.join("/")}</div>
}
Enter fullscreen mode Exit fullscreen mode

Optional Catch-All Routes

Optional version:

[[...slug]]
Enter fullscreen mode Exit fullscreen mode

This supports both:

/docs
/docs/routing
/docs/config/api
Enter fullscreen mode Exit fullscreen mode

All handled by the same page.


Understanding Route Groups

Route Groups help organize code without affecting URLs.

They use parentheses.

Example:

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

  (dashboard)/
    dashboard/
      page.tsx
Enter fullscreen mode Exit fullscreen mode

Generated URLs:

/
/about
/dashboard
Enter fullscreen mode Exit fullscreen mode

The group names never appear in the URL.

Why use them?

Because real applications often have multiple app sections.

Example:

app/
  (marketing)/
    layout.tsx
    page.tsx
    pricing/
      page.tsx

  (app)/
    layout.tsx
    dashboard/
      page.tsx
    settings/
      page.tsx
Enter fullscreen mode Exit fullscreen mode

This allows:

  • different layouts
  • different providers
  • different UI structures

without polluting URLs.


A Real-World Folder Structure

Here’s a scalable structure used in production projects.

app/
  layout.tsx
  page.tsx

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

  (dashboard)/
    dashboard/
      layout.tsx
      page.tsx
      analytics/
        page.tsx
      settings/
        page.tsx

  blog/
    page.tsx
    [slug]/
      page.tsx
Enter fullscreen mode Exit fullscreen mode

This keeps marketing pages, application UI, and content routes clearly separated.


Common Mistakes Developers Make

When I mentor developers using Next.js, these are the issues I see most often.

1. Mixing pages/ and app/

While technically possible, it creates confusion.

If you start with app/, commit to it.


2. Over-nesting folders

Deep structures become hard to maintain.

Prefer:

dashboard/analytics
Enter fullscreen mode Exit fullscreen mode

Instead of:

dashboard/features/analytics/pages
Enter fullscreen mode Exit fullscreen mode

3. Ignoring layouts

Layouts are one of the most powerful features of the App Router.

Use them.

They dramatically simplify architecture.


Final Thoughts

The app/ directory might feel unfamiliar at first.

But once it clicks, it becomes one of the cleanest ways to structure React applications.

Instead of wrestling with routing configuration, global wrappers, and layout duplication, you get a clear, predictable architecture.

  • Folders represent routes
  • special files control behavior
  • layouts manage shared UI

And suddenly your project feels a lot more… Zen.


If this article helped you understand the Next.js app/ directory better:

  • Leave a ❤️ reaction
  • Drop a 🦄 unicorn if you love Next.js
  • Share in the comments how you structure your projects

And if you enjoy content like this, feel free to follow me here on DEV for more posts about Next.js, architecture, and developer productivity.

Top comments (3)

Collapse
 
gavincettolo profile image
Gavin Cettolo

Curious to hear how others are structuring their Next.js projects 👀

What’s the biggest challenge you’ve faced with the app/ directory so far?

  • understanding routing?
  • organizing components?
  • server vs client confusion?
  • or just keeping things clean over time?

I’ve seen teams struggle especially with mixing concerns too early.

Would love to hear real-world experiences 👇

Collapse
 
paolozero profile image
Paolo Zero

Really nice breakdown of the App Router structure, I like how you made something that can feel chaotic actually look intentional and scalable.

One thing I’m still trying to fully wrap my head around: how do you usually decide when to split logic into separate route groups vs keeping things flat for simplicity?

I’ve seen both approaches in real projects, and I’m curious what signals you use to avoid over-engineering early on.

Collapse
 
gavincettolo profile image
Gavin Cettolo

Great question, this is exactly where most teams struggle.

My rule of thumb is: don’t use route groups for structure, use them for intent.

I usually keep things flat until one of these happens:

  • different layouts are needed (e.g. auth vs dashboard)
  • different access patterns (public vs private)
  • the folder starts mixing unrelated concerns At that point, route groups become really useful because they let you separate concerns without affecting the URL.

The mistake I see most often is introducing them too early “just to stay organized”, which actually makes the structure harder to understand.
Flat structure → easier to navigate

Route groups → better separation of responsibilities

So I try to optimize for simplicity first, then introduce structure only when the friction becomes real.
I hope I answered your question.