
Folder Architecture at Scale: How to Organize React, Angular, and Node.js Projects with SOLID and Clean Architecture
If you have ever had to add a simple field to a form and found yourself navigating through five folders in the project root (/components, /services, /types, /hooks, /utils), you have been a victim of file-type architecture.
At the beginning of a software project, grouping files by what they are (components with components, services with services) seems clean and intuitive. However, as the application scales to hundreds of screens and business rules, this approach turns into a cognitive nightmare. Coupling skyrockets, cohesion goes out the window, and deleting an old feature without breaking the system becomes an impossible mission.
Today, many high-performance teams in React, Angular, and Node.js converge on Domain/Feature-Scoped Organization β often loosely called Vertical Slices.
In this article, we will dissect how to structure folders for modern applications across the frontend and backend, demonstrating how the principles of SOLID, DRY, and Separation of Concerns (SoC) shape this architecture in practice.
1. The Golden Principle: Colocation and Cohesion
Before looking at specific frameworks, we need to understand the fundamental rule governing modern architectures: code that changes together should live together.
This is what we call Colocation. If the screen component for a shopping cart needs a shipping-cost hook, a specific TypeScript interface, and an API call, all of these files should live inside the Cart feature folder, rather than being scattered globally across the project.
The Architectural Acid Test: If your Product Owner asked you to remove the "Wishlist" feature today, could you delete the
/features/wishlistfolder, remove the route that uses it, and have the project compile without errors? If the answer is no, your coupling is too high.
2. The Theoretical Foundation: SOLID and DRY in the File System
A good folder structure is the physical manifestation of software engineering principles in your repository. Here is how they apply to our organization:
SOLID in Folder Practice
- S β Single Responsibility Principle (SRP): In the legacy model, a
UserControllerorGlobalServicesfile accumulated business rules, HTTP validation, and database queries. In our structure, we isolate responsibilities. In Node.js, a file inuse-cases/create-order.usecase.tshas only one reason to change: the order business rule. It ignores the HTTP protocol (thecontroller's responsibility) and the database (therepository's responsibility). - O β Open/Closed Principle (OCP): Your system should be open for extension but closed for modification. If you need to add a new payment method in the frontend, you do not edit an existing massive component; you create a new folder inside
features/billing/components/(e.g.,/pix-checkout/) with a component that implements the payment-method contract the checkout already consumes, and register it in the list of methods. The checkout, which only knows that contract, doesn't change; the only existing line that moves is the registration. - L β Liskov Substitution Principle (LSP): Any implementation of
order.repository.interface.tsβ the productionprisma-order.repository.tsor an in-memory repository in tests β must be able to stand in for the interface without the use case noticing any difference in behavior. - I β Interface Segregation Principle (ISP): We avoid the global
/typesfolder containing a 3,000-lineindex.tsfile. By placing thetypes/ordto/folder inside each specific module, we ensure that a component consumes only the strict contracts necessary for its functionality, without relying on type definitions from other areas of the system. - D β Dependency Inversion Principle (DIP): Our business rules depend on abstractions (interfaces), not concrete implementations. This allows us to swap external libraries or databases by simply replacing the file in the infrastructure layer, without rewriting the domain logic.
Strategic DRY (And the Coupling Trap)
The Don't Repeat Yourself (DRY) principle is often applied blindly by developers. In our architecture, it shows up most visibly in pure infrastructure and generic UI (/shared, /components/ui, /lib). Buttons, modals, and HTTP clients are written only once.
However, in business rules, you have to tell duplicated knowledge β which is what DRY targets β apart from shapes that merely coincide. If the OrderDTO entity in the Orders module has the exact same four fields as the InvoiceDTO in the Billing module, do not try to unify them into a BaseGlobalDTO in /shared.
Accidental duplication across different domains is far cheaper than premature coupling. If the tax department requires a new field on the invoice tomorrow, the orders module should not be impacted.
3. Angular: The Standalone Era and Practical DDD
With Standalone APIs as the default (since Angular 19) and NgModules now optional, Angular shed much of its bureaucratic complexity. The modern pattern draws heavy inspiration from Nx library types (feature, ui, data-access, util) and lightweight Domain-Driven Design (DDD) concepts.
We divide the application into four macro-categories: core, shared (or ui), features, and data-access.
Recommended Structure for Angular
src/
ββ app/
β ββ core/ # Singleton services, interceptors, and global guards
β β ββ auth/
β β β ββ auth.guard.ts
β β β ββ auth.service.ts
β β ββ http/
β β ββ error.interceptor.ts
β β
β ββ shared/ # "Dumb" and generic UI (Design System)
β β ββ ui/
β β β ββ button/
β β β ββ modal/
β β ββ utils/
β β
β ββ features/ # Business slices (Vertical Slices)
β β ββ checkout/
β β β ββ components/ # Internal components exclusive to checkout
β β β ββ services/ # Checkout presentation logic
β β β ββ checkout.routes.ts # Lazy Loaded route
β β β ββ checkout.component.ts
β β β
β β ββ catalog/
β β
β ββ data-access/ # External API communication and Global State (Signals)
β ββ products/
β β ββ products.api.ts
β β ββ products.store.ts # SignalStore / State
β β ββ products.types.ts
β ββ user/
- Separation of Concerns (SoC): The
data-accessfolder centralizes HTTP calls and state management (SignalStore). Thefeatures/layer consumes this data without knowing how it was fetched or tagged in the cache. This is a deliberate exception to the colocation rule from section 1: data used by several features (products show up in both the catalog and the checkout) gets its own folder; data owned by a single feature stays inside it β in Nx,libs/<domain>/data-access. - UI Isolation: Anything inside
features/checkout/componentsis private. Iffeatures/catalogneeds the same product card, that component must be refactored and promoted toshared/ui.
4. React: Feature-Scoped Architecture and Barrel Files
In the React ecosystem (whether Next.js with App Router or SPAs with Vite), architectural freedom often leads to traumatic refactoring. The most resilient structure adopts the Bulletproof React methodology, abandoning bloated global folders in favor of self-sufficient modules.
Recommended Structure for React
src/
ββ app/ # Routing (Next.js App Router or React Router setup)
β ββ (public)/
β ββ (authenticated)/
β ββ dashboard/
β ββ page.tsx # The page acts purely as a Feature orchestrator!
β
ββ components/ # Global UI components (Shadcn / Tailwind)
β ββ ui/
β β ββ button.tsx
β β ββ dialog.tsx
β ββ layouts/
β
ββ lib/ # External client configurations (Axios, QueryClient)
β ββ axios.ts
β ββ react-query.ts
β
ββ features/ # The core of the application
β ββ billing/
β β ββ api/ # TanStack Query hooks (useInvoice, usePay)
β β ββ components/ # InvoiceList, PaymentModal
β β ββ hooks/ # Local feature hooks (useTaxCalculator)
β β ββ types/ # DTO interfaces restricted to billing
β β ββ index.ts # BARREL FILE: Exports only what is public!
β β
β ββ projects/
β
ββ stores/ # Global state (Zustand, when strictly necessary)
ββ use-theme-store.ts
The Power of the Barrel File (index.ts)
Inside each React feature, we maintain an index.ts file at the root acting as the module's public API. (This is where we part ways with Bulletproof React, which now discourages barrel files because they hurt tree shaking in Vite; adopting the barrel means accepting that cost in exchange for an explicit boundary.) If the Dashboard page needs to render the invoice list, it imports:
import { InvoiceList } from '@/features/billing';
The barrel alone doesn't stop anyone from importing deep internal files (such as a private useTaxCalculator hook) by their full path; what enforces encapsulation and information hiding is a lint rule β import/no-restricted-paths or no-restricted-imports β that forbids importing past index.ts.
5. Node.js (NestJS / Express): The Modular Monolith
In the backend, the fundamental mistake is structuring the project by technical layers at the root (/controllers, /models, /repositories). This violates the Single Responsibility Principle at the module level.
Whether using NestJS (which natively guides us down this path) or a clean setup with Express/Fastify, we must design the backend as a Modular Monolith based on Clean Architecture.
Recommended Structure for Node.js
src/
ββ config/ # Environment variables, DB connection, loggers
ββ shared/ # Global middlewares, custom exceptions, decorators
β ββ errors/
β ββ middlewares/
β
ββ modules/ # Business domains (Bounded Contexts)
β ββ orders/
β β ββ dto/ # Data Transfer Objects (Zod / Class-Validator)
β β β ββ create-order.dto.ts
β β β ββ order-response.dto.ts
β β β
β β ββ entities/ # Domain models (no ORM types)
β β β ββ order.entity.ts
β β β
β β ββ repositories/ # Database isolation (Repository Pattern)
β β β ββ order.repository.interface.ts
β β β ββ prisma-order.repository.ts
β β β
β β ββ use-cases/ # Pure business logic (Or Services in NestJS)
β β β ββ create-order.usecase.ts
β β β ββ calculate-discount.usecase.ts
β β β
β β ββ orders.controller.ts # HTTP Layer / Data Input & Output
β β ββ orders.module.ts # Dependency wiring (Dependency Injection)
β β
β ββ users/
β
ββ main.ts # Application entrypoint
Strategic Advantages of the Modular Monolith
- Infrastructure Independence (DIP): By isolating
use-cases(business logic) fromrepositories(data access), if the company decides to migrate from MongoDB to PostgreSQL in the future, the code to change stays concentrated in/repositoriesβ as long asentities/isn't the ORM schema. The business logic remains untouched; migrating the data and handling the transaction differences between the two databases are still separate work. - Microservices-Ready: If the
ordersmodule experiences a severe processing bottleneck and needs to scale independently, boundaries are already drawn. Extracting it into an independent microservice becomes far cheaper: the domain code moves almost intact, and the real work becomes what the network introduces β calls that turn remote, partial failures, transactions that stop being local, and splitting the data.
Conclusion
Software architecture does not exist to bureaucratize engineering work with rigid rules; its main function is cognitive load management.
When we organize React, Angular, or Node.js projects by Features and implement SOLID and DRY principles at the root of our directory structure, we enable any developer to understand, modify, and test a specific slice of the system without needing to load the entire application into their short-term memory.
The next time you start a project or refactor a legacy system, abandon grouping by syntax or file type. Start structuring your code by the business problems it solves.
Comments
Loading comments...