This topic addresses one of the most debated aspects of frontend and backend development, where to put your files. As projects scale from 10 to 100+ modules, the standard "folders by type" approach often becomes a bottleneck for developer productivity.
When a project is small, organizing by type (e.g., all controllers in one folder, all components in another) feels intuitive. But as I found while building modular UIs and enterprise systems, this structure eventually leads to "Folder Sprawl."
To build for scale, we shifted from Type-based organization to Feature-based organization. Here is the blueprint for how to structure enterprise-grade apps across the stack.
1. The Debate: Type-based vs. Feature-based
The Type-based Approach
Organizing by what the file is (Components, Services, Models).
- Pros: Easy to set up, follows framework defaults.
- Cons: To work on a "User Profile" feature, you must open five different folders. It increases cognitive load and makes code discovery difficult.
The Feature-based Approach
Organizing by what the file does (Authentication, Billing, Dashboard).
- Pros: High cohesion, everything related to a feature is in one place.
- Cons: Requires more discipline and a "Shared" module for cross-cutting concerns.
2. Implementation: Nest.js
Nest.js is designed around modules, which naturally encourages a feature-based structure.
nestjs-app/
├── src/
│ ├── main.ts
│ ├── app.module.ts
│ │
│ ├── common/ # Shared across all features
│ │ ├── decorators/
│ │ ├── guards/
│ │ ├── interceptors/
│ │ ├── pipes/
│ │ ├── filters/
│ │ ├── middleware/
│ │ ├── interfaces/
│ │ └── utils/
│ │
│ ├── config/ # Configuration
│ │ ├── database.config.ts
│ │ ├── app.config.ts
│ │ └── validation.schema.ts
│ │
│ ├── features/ # Feature modules
│ │ │
│ │ ├── auth/
│ │ │ ├── auth.module.ts
│ │ │ ├── auth.controller.ts
│ │ │ ├── auth.service.ts
│ │ │ ├── dto/
│ │ │ │ ├── login.dto.ts
│ │ │ │ ├── register.dto.ts
│ │ │ │ └── refresh-token.dto.ts
│ │ │ ├── entities/
│ │ │ │ └── session.entity.ts
│ │ │ ├── guards/
│ │ │ │ ├── jwt-auth.guard.ts
│ │ │ │ └── roles.guard.ts
│ │ │ ├── strategies/
│ │ │ │ ├── jwt.strategy.ts
│ │ │ │ └── local.strategy.ts
│ │ │ └── tests/
│ │ │ ├── auth.controller.spec.ts
│ │ │ └── auth.service.spec.ts
│ │ │
│ │ ├── users/
│ │ │ ├── users.module.ts
│ │ │ ├── users.controller.ts
│ │ │ ├── users.service.ts
│ │ │ ├── users.repository.ts
│ │ │ ├── dto/
│ │ │ │ ├── create-user.dto.ts
│ │ │ │ ├── update-user.dto.ts
│ │ │ │ └── user-response.dto.ts
│ │ │ ├── entities/
│ │ │ │ └── user.entity.ts
│ │ │ ├── interfaces/
│ │ │ │ └── user.interface.ts
│ │ │ └── tests/
│ │ │ ├── users.controller.spec.ts
│ │ │ └── users.service.spec.ts
│ │ │
│ │ ├── products/
│ │ │ ├── products.module.ts
│ │ │ ├── products.controller.ts
│ │ │ ├── products.service.ts
│ │ │ ├── products.repository.ts
│ │ │ ├── dto/
│ │ │ │ ├── create-product.dto.ts
│ │ │ │ ├── update-product.dto.ts
│ │ │ │ └── product-filter.dto.ts
│ │ │ ├── entities/
│ │ │ │ ├── product.entity.ts
│ │ │ │ └── product-category.entity.ts
│ │ │ └── tests/
│ │ │
│ │ ├── orders/
│ │ │ ├── orders.module.ts
│ │ │ ├── orders.controller.ts
│ │ │ ├── orders.service.ts
│ │ │ ├── orders.repository.ts
│ │ │ ├── dto/
│ │ │ │ ├── create-order.dto.ts
│ │ │ │ └── order-item.dto.ts
│ │ │ ├── entities/
│ │ │ │ ├── order.entity.ts
│ │ │ │ └── order-item.entity.ts
│ │ │ ├── events/
│ │ │ │ └── order-created.event.ts
│ │ │ ├── listeners/
│ │ │ │ └── order-notification.listener.ts
│ │ │ └── tests/
│ │ │
│ │ └── payments/
│ │ ├── payments.module.ts
│ │ ├── payments.controller.ts
│ │ ├── payments.service.ts
│ │ ├── dto/
│ │ ├── entities/
│ │ └── tests/
│ │
│ └── database/ # Database specific
│ ├── migrations/
│ ├── seeds/
│ └── database.module.ts
│
├── test/ # E2E tests
│ └── app.e2e-spec.ts
│
├── .env
├── .env.example
├── nest-cli.json
├── package.json
├── tsconfig.json
└── README.md
Key Principles for NestJS:
- Each feature is a self-contained module with its own controllers, services, DTOs, and entities
- Shared utilities and guards go in
common/ - Database entities live within their respective feature folders
- Each feature can be independently tested and potentially extracted into a microservice
3. Implementation: Nuxt.js & Next.js (The Frontend)
Frontend projects often suffer from a components/ folder containing 200 unrelated files. For enterprise projects, I implement a "Modular UI" strategy.
Next.js Feature-First Structure
nextjs-app/
├── src/
│ ├── app/ # App Router
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── loading.tsx
│ │ ├── error.tsx
│ │ │
│ │ ├── (auth)/ # Route group
│ │ │ ├── login/
│ │ │ │ └── page.tsx
│ │ │ └── register/
│ │ │ └── page.tsx
│ │ │
│ │ ├── dashboard/
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── loading.tsx
│ │ │
│ │ ├── products/
│ │ │ ├── page.tsx
│ │ │ ├── [id]/
│ │ │ │ ├── page.tsx
│ │ │ │ └── loading.tsx
│ │ │ └── new/
│ │ │ └── page.tsx
│ │ │
│ │ └── api/ # API routes
│ │ ├── auth/
│ │ │ └── [...nextauth]/
│ │ │ └── route.ts
│ │ ├── products/
│ │ │ ├── route.ts
│ │ │ └── [id]/
│ │ │ └── route.ts
│ │ └── orders/
│ │ └── route.ts
│ │
│ ├── features/ # Feature modules
│ │ │
│ │ ├── auth/
│ │ │ ├── components/
│ │ │ │ ├── LoginForm.tsx
│ │ │ │ ├── RegisterForm.tsx
│ │ │ │ └── AuthProvider.tsx
│ │ │ ├── hooks/
│ │ │ │ ├── useAuth.ts
│ │ │ │ └── useSession.ts
│ │ │ ├── services/
│ │ │ │ └── auth.service.ts
│ │ │ ├── types/
│ │ │ │ └── auth.types.ts
│ │ │ ├── utils/
│ │ │ │ └── validation.ts
│ │ │ └── constants/
│ │ │ └── auth.constants.ts
│ │ │
│ │ ├── users/
│ │ │ ├── components/
│ │ │ │ ├── UserProfile.tsx
│ │ │ │ ├── UserList.tsx
│ │ │ │ └── UserCard.tsx
│ │ │ ├── hooks/
│ │ │ │ ├── useUser.ts
│ │ │ │ └── useUsers.ts
│ │ │ ├── services/
│ │ │ │ └── user.service.ts
│ │ │ ├── types/
│ │ │ │ └── user.types.ts
│ │ │ └── utils/
│ │ │ └── user.utils.ts
│ │ │
│ │ ├── products/
│ │ │ ├── components/
│ │ │ │ ├── ProductCard.tsx
│ │ │ │ ├── ProductList.tsx
│ │ │ │ ├── ProductDetail.tsx
│ │ │ │ ├── ProductForm.tsx
│ │ │ │ └── ProductFilters.tsx
│ │ │ ├── hooks/
│ │ │ │ ├── useProducts.ts
│ │ │ │ ├── useProduct.ts
│ │ │ │ └── useProductMutation.ts
│ │ │ ├── services/
│ │ │ │ └── product.service.ts
│ │ │ ├── types/
│ │ │ │ └── product.types.ts
│ │ │ ├── store/ # If using state management
│ │ │ │ ├── productSlice.ts
│ │ │ │ └── productSelectors.ts
│ │ │ └── utils/
│ │ │ └── product.utils.ts
│ │ │
│ │ ├── orders/
│ │ │ ├── components/
│ │ │ │ ├── OrderList.tsx
│ │ │ │ ├── OrderDetail.tsx
│ │ │ │ └── OrderSummary.tsx
│ │ │ ├── hooks/
│ │ │ │ └── useOrders.ts
│ │ │ ├── services/
│ │ │ │ └── order.service.ts
│ │ │ └── types/
│ │ │ └── order.types.ts
│ │ │
│ │ └── cart/
│ │ ├── components/
│ │ │ ├── CartDrawer.tsx
│ │ │ ├── CartItem.tsx
│ │ │ └── CartSummary.tsx
│ │ ├── hooks/
│ │ │ └── useCart.ts
│ │ ├── store/
│ │ │ └── cartSlice.ts
│ │ └── types/
│ │ └── cart.types.ts
│ │
│ ├── shared/ # Shared across features
│ │ ├── components/
│ │ │ ├── ui/
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── Input.tsx
│ │ │ │ ├── Modal.tsx
│ │ │ │ └── Card.tsx
│ │ │ ├── layout/
│ │ │ │ ├── Header.tsx
│ │ │ │ ├── Footer.tsx
│ │ │ │ ├── Sidebar.tsx
│ │ │ │ └── Container.tsx
│ │ │ └── forms/
│ │ │ ├── FormInput.tsx
│ │ │ └── FormSelect.tsx
│ │ ├── hooks/
│ │ │ ├── useDebounce.ts
│ │ │ ├── useLocalStorage.ts
│ │ │ └── useMediaQuery.ts
│ │ ├── utils/
│ │ │ ├── api.ts
│ │ │ ├── format.ts
│ │ │ └── validation.ts
│ │ ├── types/
│ │ │ └── global.types.ts
│ │ └── constants/
│ │ └── app.constants.ts
│ │
│ ├── lib/ # Third-party configurations
│ │ ├── prisma.ts
│ │ ├── axios.ts
│ │ └── react-query.ts
│ │
│ └── styles/
│ ├── globals.css
│ └── theme.ts
│
├── public/
│ ├── images/
│ ├── fonts/
│ └── icons/
│
├── prisma/ # Database schema
│ └── schema.prisma
│
├── .env.local
├── .env.example
├── next.config.js
├── tailwind.config.js
├── tsconfig.json
├── package.json
└── README.md
Key Principles for Next.js:
- Route-specific components live in
app/directory - Reusable feature logic (components, hooks, services) lives in
features/ - Shared UI components and utilities in
shared/ - Each feature is independent and can import from other features when needed
- API routes follow the same feature-based organization
Nuxt.js Feature-First Structure
nuxtjs-app/
├── app/
│ ├── app.vue
│ └── router.options.ts
│
├── assets/ # Uncompiled assets
│ ├── styles/
│ │ ├── main.css
│ │ └── variables.css
│ ├── images/
│ └── fonts/
│
├── components/ # Auto-imported components
│ ├── layout/
│ │ ├── AppHeader.vue
│ │ ├── AppFooter.vue
│ │ └── AppSidebar.vue
│ │
│ └── ui/ # Shared UI components
│ ├── BaseButton.vue
│ ├── BaseInput.vue
│ ├── BaseModal.vue
│ └── BaseCard.vue
│
├── composables/ # Auto-imported composables
│ ├── useDebounce.ts
│ ├── useLocalStorage.ts
│ └── useMediaQuery.ts
│
├── features/ # Feature modules
│ │
│ ├── auth/
│ │ ├── components/
│ │ │ ├── LoginForm.vue
│ │ │ ├── RegisterForm.vue
│ │ │ └── PasswordReset.vue
│ │ ├── composables/
│ │ │ ├── useAuth.ts
│ │ │ └── useSession.ts
│ │ ├── services/
│ │ │ └── auth.service.ts
│ │ ├── types/
│ │ │ └── auth.types.ts
│ │ ├── utils/
│ │ │ └── validation.ts
│ │ └── stores/
│ │ └── auth.store.ts
│ │
│ ├── users/
│ │ ├── components/
│ │ │ ├── UserProfile.vue
│ │ │ ├── UserList.vue
│ │ │ ├── UserCard.vue
│ │ │ └── UserAvatar.vue
│ │ ├── composables/
│ │ │ ├── useUser.ts
│ │ │ └── useUsers.ts
│ │ ├── services/
│ │ │ └── user.service.ts
│ │ ├── types/
│ │ │ └── user.types.ts
│ │ ├── utils/
│ │ │ └── user.utils.ts
│ │ └── stores/
│ │ └── user.store.ts
│ │
│ ├── products/
│ │ ├── components/
│ │ │ ├── ProductCard.vue
│ │ │ ├── ProductList.vue
│ │ │ ├── ProductDetail.vue
│ │ │ ├── ProductForm.vue
│ │ │ └── ProductFilters.vue
│ │ ├── composables/
│ │ │ ├── useProducts.ts
│ │ │ ├── useProduct.ts
│ │ │ └── useProductFilters.ts
│ │ ├── services/
│ │ │ └── product.service.ts
│ │ ├── types/
│ │ │ └── product.types.ts
│ │ ├── utils/
│ │ │ └── product.utils.ts
│ │ └── stores/
│ │ └── product.store.ts
│ │
│ ├── orders/
│ │ ├── components/
│ │ │ ├── OrderList.vue
│ │ │ ├── OrderDetail.vue
│ │ │ ├── OrderSummary.vue
│ │ │ └── OrderStatus.vue
│ │ ├── composables/
│ │ │ └── useOrders.ts
│ │ ├── services/
│ │ │ └── order.service.ts
│ │ ├── types/
│ │ │ └── order.types.ts
│ │ └── stores/
│ │ └── order.store.ts
│ │
│ └── cart/
│ ├── components/
│ │ ├── CartDrawer.vue
│ │ ├── CartItem.vue
│ │ └── CartSummary.vue
│ ├── composables/
│ │ └── useCart.ts
│ ├── types/
│ │ └── cart.types.ts
│ └── stores/
│ └── cart.store.ts
│
├── layouts/ # Application layouts
│ ├── default.vue
│ ├── auth.vue
│ └── dashboard.vue
│
├── middleware/ # Route middleware
│ ├── auth.ts
│ ├── guest.ts
│ └── permissions.ts
│
├── pages/ # File-based routing
│ ├── index.vue
│ │
│ ├── auth/
│ │ ├── login.vue
│ │ └── register.vue
│ │
│ ├── dashboard/
│ │ └── index.vue
│ │
│ ├── products/
│ │ ├── index.vue
│ │ ├── [id].vue
│ │ └── new.vue
│ │
│ └── orders/
│ ├── index.vue
│ └── [id].vue
│
├── plugins/ # Nuxt plugins
│ ├── api.ts
│ └── toast.ts
│
├── public/ # Static files
│ ├── favicon.ico
│ └── robots.txt
│
├── server/ # Server-side code
│ ├── api/
│ │ ├── auth/
│ │ │ ├── login.post.ts
│ │ │ └── register.post.ts
│ │ ├── products/
│ │ │ ├── index.get.ts
│ │ │ ├── [id].get.ts
│ │ │ └── [id].put.ts
│ │ └── orders/
│ │ ├── index.get.ts
│ │ └── [id].get.ts
│ │
│ ├── middleware/
│ │ └── auth.ts
│ │
│ └── utils/
│ └── db.ts
│
├── stores/ # Global Pinia stores
│ └── app.store.ts
│
├── types/ # Global TypeScript types
│ └── index.d.ts
│
├── utils/ # Auto-imported utilities
│ ├── api.ts
│ ├── format.ts
│ └── constants.ts
│
├── .env
├── .env.example
├── nuxt.config.ts
├── tailwind.config.js
├── tsconfig.json
├── package.json
└── README.md
Key Principles for Nuxt.js:
- File-based routing in
pages/directory - Feature-specific components in
features/[feature]/components/ - Auto-imported composables from both root
composables/and feature-specific directories - Server API routes follow feature organization in
server/api/ - Pinia stores for state management within each feature
- Shared components in root
components/directory with auto-import
4. Cross-Cutting Concerns: The Shared Module
The biggest risk of feature-based structures is duplication. To solve this, you must have a strictly governed shared or common folder.
- Shared UI: Generic buttons, inputs, and layouts.
- Shared Utils: Date formatters, string manipulators, and validators.
- Shared Hooks:
useWindowSize,useAuthContext.
The Rule: A feature can import from shared, but shared can never import from a feature.
5. Common Benefits of Feature-First Architecture
- Scalability: Easy to add new features without affecting existing ones
- Maintainability: Related code is co-located, making it easier to understand and modify
- Team Collaboration: Different teams can work on different features with minimal conflicts
- Code Reusability: Shared code is explicitly separated from feature-specific code
- Testing: Each feature can be tested independently
- Modularity: Features can potentially be extracted into separate packages or microservices
6. Migration Tips
When transitioning from a layer-first to feature-first structure:
- Start with new features using the feature-first approach
- Gradually migrate existing features one at a time
- Keep shared utilities separate from the start
- Use barrel exports (index files) to maintain clean import paths
- Document the structure for your team
Configure path aliases in your tsconfig.json:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@features/*": ["./src/features/*"],
"@shared/*": ["./src/shared/*"],
"@common/*": ["./src/common/*"]
}
}
}
5. Summary: Scalability Checklist
| Principle | Description |
|---|---|
| Scale-Ready Structure | Design the folder structure to accommodate growth without major refactoring |
| Locality | Keep logic, types, and styles together with the component/module |
| Encapsulation | Use index.ts files to export only what is necessary (The Public API pattern) |
| Dependency Rule | Features should rarely depend on other features; use a Service or Store for communication |
| Naming | Use consistent suffixes (e.g., user.controller.ts, user.service.ts) for easy searching |
Conclusion
Structure is not just about aesthetics, it is about developer velocity. By organizing Nest.js, Nuxt.js, and Next.js around features rather than types, you reduce the "mental tax" of navigating the codebase. This allows your team to spend less time finding files and more time building features.
Top comments (0)