When a Next.js project is small, almost any folder structure works.
But as the application grows, things start getting messy:
- Where should this component live?
- Should this hook be global or feature-specific?
- Where should API logic go?
- Where should business logic live?
- Should this page be a Server Component or Client Component?
- When should I create a new folder?
These decisions may seem small at first, but they have a big impact on maintainability as the project grows.
In this article, I'll share a practical folder structure I use for medium-to-large Next.js applications using the App Router.
The main idea is simple:
Keep routes focused on routing, features focused on business logic, and shared folders truly shared.
The structure
my-next-app/
├── public/
│
├── src/
│ ├── app/
│ │ ├── (auth)/
│ │ │ ├── login/
│ │ │ └── register/
│ │ │
│ │ ├── (dashboard)/
│ │ │ ├── dashboard/
│ │ │ ├── projects/
│ │ │ └── settings/
│ │ │
│ │ ├── api/
│ │ │ └── projects/
│ │ │ └── route.ts
│ │ │
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ ├── error.tsx
│ │ └── not-found.tsx
│ │
│ ├── features/
│ │ ├── auth/
│ │ ├── projects/
│ │ ├── users/
│ │ └── billing/
│ │
│ ├── components/
│ │ ├── ui/
│ │ ├── layout/
│ │ └── shared/
│ │
│ ├── lib/
│ │ ├── db/
│ │ ├── auth/
│ │ ├── api/
│ │ ├── validation/
│ │ └── logger/
│ │
│ ├── hooks/
│ ├── config/
│ ├── types/
│ └── styles/
│
├── next.config.ts
├── package.json
└── tsconfig.json
Why feature-based architecture?
Instead of organizing everything by technical type:
components/
hooks/
services/
schemas/
I prefer organizing application-specific code by feature:
features/
├── auth/
├── projects/
├── users/
└── billing/
For example:
features/projects/
├── components/
├── hooks/
├── services/
├── schemas/
├── types.ts
└── index.ts
Now, when I need to work on the projects feature, most of the relevant code is in one predictable place.
This also makes ownership much clearer.
Keep app/ focused on routing
One of the mistakes I see in growing Next.js applications is putting too much business logic inside app/.
I prefer keeping the App Router responsible mainly for:
- Routes
- Layouts
- Loading states
- Error states
- Route handlers
- Page composition
For example:
export default async function ProjectsPage() {
const projects = await getProjects();
return <ProjectsView projects={projects} />;
}
The route defines where the page exists.
The feature defines what the page does.
Keep shared components actually shared
Your global components/ directory should not become a dumping ground.
Good candidates:
components/
├── ui/
│ ├── button.tsx
│ ├── input.tsx
│ └── dialog.tsx
│
├── layout/
│ ├── header.tsx
│ ├── sidebar.tsx
│ └── footer.tsx
│
└── shared/
├── empty-state.tsx
├── loading-state.tsx
└── error-state.tsx
If only one feature uses a component, I generally keep it inside that feature.
features/projects/components/project-card.tsx
instead of:
components/project-card.tsx
This simple rule can prevent your global components folder from becoming difficult to maintain.
Server Components by default
With the App Router, I generally start with Server Components.
A component should become a Client Component when it actually needs things such as:
useStateuseEffect- Browser APIs
- Event handlers
- Client-only libraries
- Interactive UI
And even then, I try to keep the client boundary as small as possible.
For example:
Dashboard
├── Server Component
│ ├── Data fetching
│ └── Statistics
│
└── Client Component
└── InteractiveChart
You don't necessarily need to turn the entire page into a Client Component just because one small part needs interactivity.
Where should API logic live?
I like separating HTTP concerns from business logic.
For example:
app/api/projects/route.ts
↓
features/projects/services/create-project.ts
↓
lib/db/
The route handler deals with the HTTP layer.
The feature service handles the business logic.
The infrastructure layer handles things such as database access.
This separation makes the application easier to test and change later.
Don't create a giant utils/ folder
We've all seen this:
utils/
├── helper.ts
├── common.ts
├── format.ts
├── misc.ts
└── something.ts
The problem isn't the folder itself.
The problem is that ownership becomes unclear.
Instead, ask:
Who owns this code?
If it's feature-specific:
features/projects/
If it's infrastructure:
lib/
If it's genuinely reusable UI:
components/
If it's a globally reusable hook:
hooks/
Good architecture isn't about having more folders.
It's about making ownership obvious.
Final thoughts
There isn't one perfect Next.js folder structure.
A small application doesn't need an enterprise architecture.
But once your application starts growing, having clear boundaries becomes extremely valuable.
The principles I try to follow are:
- Keep
app/focused on routing. - Organize business functionality by feature.
- Keep shared components genuinely shared.
- Prefer Server Components by default.
- Keep Client Components small.
- Separate API/HTTP concerns from business logic.
- Keep feature-specific schemas and types close to the feature.
- Avoid giant
utils/,hooks/, andtypes/folders. - Optimize for discoverability and ownership.
- Don't over-engineer before you actually need the structure.
The goal isn't to create the most complicated architecture.
The goal is to create an architecture where the next developer can quickly answer: "Where does this code belong?"
I've written a more detailed version with the complete folder structure and explanations here:
👉 https://www.vrushikvisavadiya.com/blog/best-nextjs-folder-structure-for-large-scale-applications
If you have a different approach to structuring large Next.js applications, I'd love to hear it.
How do you structure your Next.js projects?
Top comments (3)
How do you handle icons in that structure? I’d keep the raw SVG primitive shared, but feature-specific icon semantics close to the feature that owns them.
Yes, definitely. For Next.js, I prefer keeping reusable UI icons in
src/components/icons, whilepublic/is better for static assets that need to be accessed by URL.Some comments may only be visible to logged-in visitors. Sign in to view all comments.