DEV Community

ThankGod Chibugwum Obobo
ThankGod Chibugwum Obobo

Posted on • Originally published at actocodes.hashnode.dev

Scalable Folder Structures: How I Organized Nest.js, Nuxt.js and Next.js for Enterprise Projects

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Scalability: Easy to add new features without affecting existing ones
  2. Maintainability: Related code is co-located, making it easier to understand and modify
  3. Team Collaboration: Different teams can work on different features with minimal conflicts
  4. Code Reusability: Shared code is explicitly separated from feature-specific code
  5. Testing: Each feature can be tested independently
  6. Modularity: Features can potentially be extracted into separate packages or microservices

6. Migration Tips

When transitioning from a layer-first to feature-first structure:

  1. Start with new features using the feature-first approach
  2. Gradually migrate existing features one at a time
  3. Keep shared utilities separate from the start
  4. Use barrel exports (index files) to maintain clean import paths
  5. 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/*"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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)