As a Next.js project grows, its folder structure becomes more than a matter of preference — it directly affects maintainability, onboarding, and development speed.
One of the most common mistakes in early-stage or less experienced projects is organizing code by file type instead of by functionality. While this may look tidy at first, it quickly breaks down as the codebase grows.
In this article, we’ll look at why file-type–based structures don’t scale well, and how a feature-based folder structure leads to cleaner, more maintainable Next.js applications.
The Problem with File-Type–Based Folder Structures
A typical beginner-friendly structure often looks like this:
components/
hooks/
types/
utils/
services/At first glance, this seems organized. Each concern has its own place. However, once the application grows, several problems emerge:
components/becomes a dumping ground for unrelated UI- Files that belong to the same feature are scattered across multiple folders
- Refactoring a feature requires touching many unrelated directories
- Understanding a feature requires mental context switching
This structure optimizes for file categorization, not for how the application actually works.
Code Should Be Organized Around What Changes Together
A more scalable way to think about structure is this:
Code should be organized around the reason it changes.
When you modify a feature, you typically change:
- its UI
- its data fetching logic
- its hooks
- its types
- its utility functions
If these pieces live in different top-level folders, every change becomes slower and riskier.
This is where a feature-based folder structure shines.
Feature-Based Folder Structure: The Core Idea
Instead of grouping files by type, group them by feature or route.
Each feature becomes a small, self-contained module that owns:
- its components
- its hooks
- its types
- its data access logic
- its utilities
Everything related to that feature lives in one place.
This approach aligns naturally with Next.js App Router, where routes already represent distinct areas of functionality.
A Practical Example: Dashboard Feature
Let’s take a common example — a dashboard route.
Using a feature-based structure, it could look like this:
app/
└─ dashboard/
├─ page.tsx
├─ layout.tsx
├─ _components/
│ ├─ StatsCard.tsx
│ └─ RecentActivity.tsx
├─ _hooks/
│ └─ useDashboardStats.ts
├─ _types/
│ └─ dashboard.ts
├─ _actions/
│ └─ getDashboardData.ts
└─ _utils/
└─ formatStats.tsEverything needed for the dashboard lives inside the dashboard folder. There is no need to search across the project to understand or modify this feature.
Why the _ Prefix Matters
Using a leading underscore (_components, _hooks, etc.) is a simple but effective convention.
It communicates intent:
- These folders are internal to the feature
- They are not meant to be imported from elsewhere
- They are implementation details, not public APIs
If you find yourself importing from another feature’s _components folder, it’s usually a sign that something should be extracted into a shared module instead.
This small convention helps enforce boundaries without additional tooling.
What Belongs in Shared Folders?
A feature-based structure does not mean everything must be duplicated.
Some code is truly shared and should live outside feature folders.
Typical examples include:
components/
└─ ui/
├─ Button.tsx
├─ Input.tsx
└─ Modal.tsx
lib/
├─ auth.ts
├─ db.ts
└─ env.tsRules of thumb for shared code:
- It should be generic
- It should not contain business logic
- It should not depend on a specific feature
Shared UI components are especially common and useful, as long as they remain unopinionated.
Why This Structure Scales Better
A feature-based folder structure provides several long-term benefits:
Easier Maintenance
All related code lives in one place. Modifying or refactoring a feature is straightforward and localized.
Faster Onboarding
New developers can navigate the codebase by following application features, not arbitrary folder categories.
Fewer Side Effects
Changes are less likely to impact unrelated parts of the application.
Easy Feature Removal
If removing a feature requires deleting many files across the project, the feature was not well isolated.
A good rule of thumb:
If deleting a feature isn’t easy, it probably wasn’t properly encapsulated.
How This Fits with Next.js App Router
Next.js App Router is particularly well suited to this approach:
- Routes already define functional boundaries
- Server Actions can live close to the feature that uses them
- Layouts and nested routes naturally group related concerns
Next.js intentionally avoids enforcing a strict architecture. This flexibility is powerful, but it also means developers must be deliberate about structure.
A feature-based approach provides clarity without fighting the framework.
When This Structure Might Be Overkill
This approach is not mandatory for every project.
It may be unnecessary for:
- very small landing pages
- short-lived prototypes
- simple marketing sites
However, once an application has multiple routes, shared state, and evolving requirements, the benefits quickly outweigh the initial setup cost.
Final Thoughts
Folder structure is not about aesthetics — it’s about how easily you can reason about your codebase.
Organizing a Next.js project by feature rather than by file type:
- aligns code with application behavior
- reduces cognitive load
- improves long-term maintainability
As projects grow, this structure helps ensure that complexity remains manageable instead of spreading across the entire codebase.
Organizing by feature aligns your codebase with how your application actually works — not just how files happen to be named.
