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, andnot-found.tsxdefine 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
- The Core Concept: File-Based Routing
- The Essential Files Inside app/
- Layouts: The Secret Weapon
- Loading and Error States
- Error Boundaries: One Important Limitation
- Handling 404 Pages with not-found.tsx
- Dynamic Routes
- Understanding Route Groups
- A Real-World Folder Structure
- Common Mistakes Developers Make
- Final Thoughts
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
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
This produces:
/ → page.tsx
/about → about/page.tsx
/dashboard → dashboard/page.tsx
A basic page file looks like this:
export default function Page() {
return <h1>Hello Next.js</h1>
}
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>
}
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
Example layout:
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<main>
<nav>Navigation</nav>
{children}
</main>
)
}
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
Rendering flow:
Root Layout
↓
Dashboard Layout
↓
Analytics Page
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>
}
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>
}
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>
)
}
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>
}
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>
)
}
To trigger it, use the notFound() function.
import {notFound} from "next/navigation"
if (!post) {
notFound()
}
You can define multiple not-found.tsx files at different levels.
Example:
app/
not-found.tsx
dashboard/
not-found.tsx
The closest one in the route tree wins.
Dynamic Routes
Dynamic routes use square brackets.
Example:
app/
blog/
[slug]/
page.tsx
URLs generated:
/blog/my-first-post
/blog/nextjs-routing
/blog/react-performance
Example implementation:
export default function BlogPost({
params
}: {
params: { slug: string }
}) {
return <h1>{params.slug}</h1>
}
You can also combine it with static generation.
Multiple Dynamic Segments
Example:
app/
shop/
[category]/
[product]/
page.tsx
Routes:
/shop/laptops/macbook-pro
/shop/phones/iphone-15
Example code:
export default function ProductPage({
params
}: {
params: {
category:string
product:string
}
}) {
return (
<div>
<h1>{params.product}</h1>
<p>Category: {params.category}</p>
</div>
)
}
Catch-All Routes
Sometimes you need flexible routes.
Next.js supports catch-all segments.
[...slug]
Example:
app/
docs/
[...slug]/
page.tsx
Supported URLs:
/docs
/docs/getting-started
/docs/guides/routing
/docs/api/config
Implementation:
export default function DocsPage({
params
}: {
params: { slug: string[] }
}) {
return <div>{params.slug?.join("/")}</div>
}
Optional Catch-All Routes
Optional version:
[[...slug]]
This supports both:
/docs
/docs/routing
/docs/config/api
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
Generated URLs:
/
/about
/dashboard
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
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
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
Instead of:
dashboard/features/analytics/pages
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)
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?I’ve seen teams struggle especially with mixing concerns too early.
Would love to hear real-world experiences 👇
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.
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:
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.