Next.js App Router: Project Structure for Production Apps
A production-tested Next.js App Router folder structure with the reasoning behind every decision. Server vs client components, route groups, and patterns that scale.

Why Structure Matters More in App Router
The Next.js App Router changed how project structure works. In the old Pages Router, structure was simple: files in pages/ became routes, everything else went wherever you wanted. The App Router added server components, nested layouts, route groups, loading states, error boundaries, and co-located data fetching. All of these are powerful features. All of them create decisions about where things go that did not exist before.
Most guides show you a toy structure with three routes and a components folder. That works for a tutorial. It does not work at 50+ routes, 100+ components, and a team of developers who need to find things without asking someone where they are. This guide shows the structure we use on production web applications and why each decision was made.
By the end, you will have a complete, annotated project structure that handles server and client components, shared layouts, route organization, and component architecture in a way that scales without becoming a mess.
Prerequisites
- Node.js 18 or later
- Next.js 14+ with the App Router enabled (this is the default for new projects)
- Familiarity with App Router basics. You should know what
page.tsx,layout.tsx, andloading.tsxdo. If you have not used the App Router before, start with the Next.js docs and come back.
The Top-Level Structure
Here is the full top-level structure we use. Every directory has a purpose, and nothing is there because "it seemed like a good idea."
# Production Next.js App Router project structure
src/
├── app/ # Routes, layouts, and page-level concerns
├── components/ # Reusable UI components
├── lib/ # Utilities, helpers, and shared logic
├── types/ # TypeScript type definitions
├── styles/ # Global styles and CSS variables
├── hooks/ # Custom React hooks
└── sections/ # Page-level section components
content/ # MDX content (blog posts, guides, etc.)
public/ # Static assets (images, fonts, favicons)Why src/?
Next.js supports both root-level and src/ directory structures. We use src/ because it separates application code from configuration files. Your tsconfig.json, next.config.js, package.json, and .eslintrc all live at the root. Your actual application code lives in src/. When you open the project, you immediately see the difference between "config" and "code." It also enables cleaner path aliases: @/components resolves to src/components.
Route Organization
The app/ directory is where the App Router magic happens. Every folder can contain a page.tsx (the route), a layout.tsx (wrapping UI), a loading.tsx (loading state), and an error.tsx (error boundary).
# Route structure for a typical production app
src/app/
├── layout.tsx # Root layout (html, body, global providers)
├── page.tsx # Homepage
├── not-found.tsx # Custom 404 page
├── globals.css # Global stylesheet
│
├── (marketing)/ # Route group: marketing pages
│ ├── about/
│ │ └── page.tsx
│ ├── pricing/
│ │ └── page.tsx
│ └── contact/
│ └── page.tsx
│
├── blog/
│ ├── page.tsx # Blog listing page
│ └── [slug]/
│ └── page.tsx # Individual blog post
│
├── dashboard/
│ ├── layout.tsx # Dashboard layout (sidebar, nav)
│ ├── page.tsx # Dashboard home
│ ├── settings/
│ │ └── page.tsx
│ └── projects/
│ ├── page.tsx # Projects list
│ └── [id]/
│ └── page.tsx # Individual project
│
└── api/
└── contact/
└── route.ts # API route handlerRoute Groups
Route groups are directories wrapped in parentheses like (marketing). They organize routes without adding a URL segment. The pages inside (marketing)/about/ are served at /about, not /marketing/about.
Use route groups when multiple pages share a logical grouping but should not share a URL prefix. Marketing pages, authenticated pages, and public pages are common groupings.
Route groups can also have their own layout.tsx. This is how you apply different layouts to different sections of your site without nesting them in the URL:
// src/app/(marketing)/layout.tsx
// This layout wraps all marketing pages but does not
// affect the URL structure
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="marketing-layout">
<MarketingNav />
<main>{children}</main>
<Footer />
</div>
);
}// src/app/dashboard/layout.tsx
// Dashboard pages get a completely different layout
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard-layout">
<Sidebar />
<main className="dashboard-main">{children}</main>
</div>
);
}Dynamic Routes
Dynamic segments use square brackets: [slug], [id]. The parameter is available in the page component's props:
// src/app/blog/[slug]/page.tsx
// The slug parameter comes from the URL
interface BlogPostPageProps {
params: Promise<{ slug: string }>;
}
export default async function BlogPostPage({
params,
}: BlogPostPageProps) {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) notFound();
return <BlogPostLayout {...post} />;
}When to Co-locate vs Separate
Co-locate when a file is only used by one route. A loading state that only applies to the dashboard belongs in src/app/dashboard/loading.tsx, not in a shared folder.
Separate when a file is used across multiple routes. A component used on three different pages belongs in src/components/, not duplicated in each route folder.
The rule is simple: if moving the file would break only one route, it belongs with that route. If moving it would break multiple routes, it belongs in a shared location.
Server vs Client Components
This is the decision that confuses people most. The App Router defaults to server components. You add "use client" to opt into client-side rendering. The question is where to draw that line.
The Decision Framework
Server component (default): Use when the component does not need browser APIs, event handlers, useState, useEffect, or useContext. This includes layouts, data display, static content, and anything that just renders props.
Client component: Use when the component needs interactivity. Click handlers, form state, animations, browser APIs (window, document), React hooks that use state or effects. Add "use client" at the top of the file.
// src/components/UserCard/UserCard.tsx
// Server component: just displays data, no interactivity needed
interface UserCardProps {
name: string;
role: string;
avatar: string;
}
export function UserCard({ name, role, avatar }: UserCardProps) {
return (
<div className="user-card">
<img src={avatar} alt="" width={48} height={48} />
<h3>{name}</h3>
<p>{role}</p>
</div>
);
}// src/components/LikeButton/LikeButton.tsx
// Client component: needs click handler and state
"use client";
import { useState } from "react";
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? "Liked" : "Like"}
</button>
);
}Where Client Components Live
Client components go in src/components/ just like server components. The "use client" directive at the top of the file is the only difference. We do not separate them into different directories. The component's location should be based on what it does, not whether it runs on the server or client.
One important pattern: keep the "use client" boundary as low in the component tree as possible. A page can be a server component that renders a client component for just the interactive part:
// src/app/blog/[slug]/page.tsx
// Server component page that includes a client component
import { LikeButton } from "@/components/LikeButton";
import { CommentSection } from "@/components/CommentSection";
export default async function BlogPostPage({ params }) {
const { slug } = await params;
const post = await getPostBySlug(slug);
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
<LikeButton postId={post.id} />
<CommentSection postId={post.id} />
</article>
);
}The page itself is a server component. It fetches data on the server. Only LikeButton and CommentSection ship JavaScript to the client. This is the pattern that keeps your bundle size small.
Component Organization
# Component folder structure
src/components/
├── Button/
│ ├── Button.tsx # Component implementation
│ ├── Button.css # Component styles
│ └── index.ts # Barrel export
├── Navigation/
│ ├── Navigation.tsx
│ ├── Navigation.css
│ └── index.ts
├── Modal/
│ ├── Modal.tsx
│ ├── Modal.css
│ └── index.ts
└── MDXComponents/
├── MDXComponents.tsx
├── MDXComponents.css
└── index.tsOne Folder Per Component
Every component gets its own folder containing the component file, its styles, and a barrel export. This keeps related files together and makes components easy to find, move, and delete. When you remove a component, you delete one folder. Nothing else in the project references files inside that folder except through the barrel export.
The barrel export (index.ts) re-exports the component so imports stay clean:
// src/components/Button/index.ts
export { Button } from "./Button";// Importing from anywhere in the project
import { Button } from "@/components/Button";Feature-Based vs Layer-Based
Layer-based organization groups by type: all components in components/, all hooks in hooks/, all types in types/. This is what we use. It works well when your components are reusable across multiple features.
Feature-based organization groups by feature: everything for the dashboard in features/dashboard/, everything for auth in features/auth/. This works better for large applications where features are relatively independent.
There is no universally correct answer. We default to layer-based because most of the applications we build have components that appear across multiple features. A Button, Modal, or Card component does not belong to any one feature.
Shared Utilities and Libraries
# Utility and library structure
src/lib/
├── utils.ts # General utility functions
├── constants.ts # App-wide constants
├── api.ts # API client and fetch helpers
└── content.ts # Content loading utilities (MDX, etc.)Keep lib/ flat for small to medium projects. If you have more than 10-15 files, organize by domain:
# Organized lib/ for larger projects
src/lib/
├── api/
│ ├── client.ts
│ └── endpoints.ts
├── auth/
│ ├── session.ts
│ └── permissions.ts
└── utils/
├── formatting.ts
└── validation.tsTypes
# Type definitions
src/types/
├── blog.ts # Blog post types and frontmatter
├── guide.ts # Guide types
├── case-study.ts # Case study types
└── api.ts # API request/response typesTypes that are specific to a single component can live in the component file itself. Types that are shared across multiple files get their own file in types/. The threshold is simple: if you import a type in more than one file, move it to types/.
Content and Data
# Content directory (outside src/)
content/
├── blog/
│ ├── first-post.mdx
│ └── second-post.mdx
├── guides/
│ ├── setup-guide.mdx
│ └── deployment-guide.mdx
└── case-studies/
└── client-project.mdxContent lives outside src/ because it is not application code. It is data. Blog posts, guides, and case studies are written in MDX and loaded by utility functions in src/lib/. This separation means content authors (or AI tools generating content) do not need to touch application code.
Data Fetching Patterns
In the App Router, data fetching happens in server components. The general pattern:
// src/app/blog/page.tsx
// Data fetching at the page level, passed down as props
import { getAllPosts } from "@/lib/content";
import { BlogList } from "@/components/BlogList";
export default async function BlogPage() {
const posts = await getAllPosts();
return <BlogList posts={posts} />;
}Fetch at the page level. Pass data down as props. Components receive data, they do not fetch it. This keeps data flow predictable and makes components testable without mocking fetch calls.
Configuration Files
# Root-level configuration
├── next.config.ts # Next.js configuration
├── tsconfig.json # TypeScript configuration
├── .eslintrc.json # ESLint rules
├── package.json # Dependencies and scripts
├── .claude/ # Claude Code settings
│ └── settings.json
└── CLAUDE.md # Claude Code project contextPath Aliases
Set up path aliases in tsconfig.json so imports are clean and refactoring is easier:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}This lets you write import { Button } from "@/components/Button" instead of import { Button } from "../../../components/Button". Every import starts with @/ and maps to src/. No more counting ../ levels.
The Full Structure Reference
Here is the complete annotated structure:
# Complete production Next.js App Router structure
project-root/
│
├── src/
│ ├── app/ # Routes and page-level concerns
│ │ ├── layout.tsx # Root layout
│ │ ├── page.tsx # Homepage
│ │ ├── not-found.tsx # 404 page
│ │ ├── globals.css # Global styles
│ │ ├── (marketing)/ # Route group (no URL prefix)
│ │ ├── blog/ # Blog routes
│ │ ├── dashboard/ # Dashboard routes (own layout)
│ │ └── api/ # API route handlers
│ │
│ ├── components/ # Reusable UI components
│ │ ├── Button/ # One folder per component
│ │ ├── Modal/
│ │ ├── Navigation/
│ │ └── Footer/
│ │
│ ├── sections/ # Page-level section compositions
│ │ ├── Hero/
│ │ └── Features/
│ │
│ ├── lib/ # Utilities and shared logic
│ │ ├── utils.ts
│ │ ├── api.ts
│ │ └── content.ts
│ │
│ ├── hooks/ # Custom React hooks
│ │ └── useMediaQuery.ts
│ │
│ ├── types/ # Shared TypeScript types
│ │ ├── blog.ts
│ │ └── api.ts
│ │
│ └── styles/ # Global styles and tokens
│ └── variables.css
│
├── content/ # MDX content files
│ ├── blog/
│ ├── guides/
│ └── case-studies/
│
├── public/ # Static assets
│ ├── images/
│ ├── fonts/
│ └── favicon.ico
│
├── next.config.ts
├── tsconfig.json
├── package.json
└── CLAUDE.mdCommon Questions
What people usually ask about Next.js project structure.
Use src/. It separates your application code from configuration files, makes path aliases simpler (@/ always means src/), and keeps the project root clean. The Next.js docs support both approaches, but src/ scales better as your project grows.
Use route groups when pages share a logical grouping (marketing pages, authenticated pages) but should not share a URL prefix. Use nested routes when the URL structure should reflect the hierarchy (dashboard/settings, dashboard/projects). Route groups organize your code. Nested routes organize your URLs.
Technically yes, but we recommend against it for most projects. Co-locating a component with its route works when that component is truly only used on one page. But components tend to get reused, and moving them later creates churn. Start in src/components/ and you will not have to move things around when requirements change.
Server components cannot use React context or state hooks. For data that both server and client components need, fetch it in a server component and pass it down as props. For client-side state that needs to be shared across components, use React context with a provider that wraps the relevant section of your component tree. Keep the provider as low in the tree as possible.
For monorepos, shared components and utilities move into packages (e.g., packages/ui, packages/utils) and each Next.js app follows this same structure internally. Tools like Turborepo or Nx manage the workspace. The per-app structure does not change, only the shared code lives outside the app.