
Arquitectura de Carpetas a Escala: Cómo organizar proyectos React, Angular y Node.js con SOLID y Clean Architecture
Si alguna vez necesitaste agregar un simple campo en un formulario y tuviste que navegar por cinco carpetas en la raíz del proyecto (/components, /services, /types, /hooks, /utils), fuiste víctima de la arquitectura por tipo de archivo.
Al inicio de un software, agrupar archivos por lo que son (componentes con componentes, servicios con servicios) parece limpio e intuitivo. Pero a medida que la aplicación escala a cientos de pantallas y reglas de negocio, este enfoque se transforma en una pesadilla cognitiva. El acoplamiento se dispara, la cohesión desaparece y eliminar una funcionalidad antigua sin romper el sistema se convierte en una misión imposible.
Hoy en día, muchos equipos de alto rendimiento en React, Angular y Node.js convergen en la Organización por Dominio/Feature, lo que suele llamarse, de forma libre, Vertical Slices (Cortes Verticales).
En este artículo, analizaremos a fondo cómo estructurar carpetas para aplicaciones modernas en el frontend y el backend, demostrando cómo los principios de SOLID, DRY y Separation of Concerns (SoC) moldean esta arquitectura en la práctica.
1. El Principio de Oro: Colocation y Cohesión
Antes de ver frameworks específicos, necesitamos entender la regla fundamental que rige las arquitecturas modernas: el código que cambia junto debe vivir junto.
Esto es lo que llamamos Colocation (Colocación). Si el componente de pantalla de un carrito de compras necesita un hook para calcular el envío, una interfaz TypeScript específica y una llamada a la API, todos esos archivos deben vivir dentro de la carpeta de la feature del Carrito, y no dispersos globalmente por el proyecto.
El Test de Ácido de la Arquitectura: Si tu Product Owner te pidiera eliminar la funcionalidad de "Lista de Deseos" hoy, ¿podrías borrar la carpeta
/features/wishlist, quitar la ruta que la usa y que el proyecto compile sin errores? Si la respuesta es no, tu acoplamiento es demasiado alto.
2. La Fundación Teórica: SOLID y DRY en el Sistema de Archivos
Una buena estructura de carpetas es la manifestación física de los principios de ingeniería de software en tu repositorio. Veamos cómo se aplican a nuestra organización:
El SOLID en la Práctica de las Carpetas
- S — Single Responsibility Principle (SRP): En el modelo legado, un archivo
UserControlleroGlobalServicesacumulaba reglas de negocio, validación HTTP y consultas a bases de datos. En nuestra estructura, aislamos las responsabilidades. En Node.js, un archivo enuse-cases/create-order.usecase.tstiene solo una razón para cambiar: la regla de negocio del pedido. Ignora el protocolo HTTP (responsabilidad delcontroller) y la base de datos (responsabilidad delrepository). - O — Open/Closed Principle (OCP): Tu sistema debe estar abierto a la extensión, pero cerrado a la modificación. Si necesitas agregar un nuevo método de pago en el frontend, no editas un componente gigante existente; creas una nueva carpeta dentro de
features/billing/components/(ej:/pix-checkout/) con un componente que implementa el contrato de método de pago que el checkout ya consume, y lo registras en la lista de métodos. El checkout, que solo conoce ese contrato, no cambia; la única línea existente que se toca es el registro. - L — Liskov Substitution Principle (LSP): Cualquier implementación de
order.repository.interface.ts—laprisma-order.repository.tsde producción o un repositorio en memoria en los tests— debe poder sustituir a la interfaz sin que el use case note ninguna diferencia de comportamiento. - I — Interface Segregation Principle (ISP): Evitamos la carpeta global
/typescon un archivoindex.tsde 3.000 líneas. Al colocar la carpetatypes/odto/dentro de cada módulo específico, garantizamos que un componente consuma únicamente los contratos estrictamente necesarios para su funcionamiento, sin depender de tipados de otras áreas del sistema. - D — Dependency Inversion Principle (DIP): Nuestras reglas de negocio dependen de abstracciones (interfaces), no de implementaciones concretas. Esto nos permite cambiar librerías externas o bases de datos simplemente sustituyendo el archivo en la capa de infraestructura, sin reescribir la lógica de dominio.
El DRY Estratégico (Y la Trampa del Acoplamiento)
El principio Don't Repeat Yourself (DRY) es aplicado a ciegas por muchos desarrolladores. En nuestra arquitectura, se ve sobre todo en la infraestructura pura y la UI genérica (/shared, /components/ui, /lib). Botones, modales y clientes HTTP se escriben una sola vez.
Sin embargo, en las reglas de negocio hay que distinguir la duplicación de conocimiento —que es lo que combate el DRY— de la simple coincidencia de forma. Si la entidad OrderDTO en el módulo de Pedidos tiene exactamente los mismos cuatro campos que la InvoiceDTO en el módulo de Facturación, no intentes unificarlas en un BaseGlobalDTO en /shared.
La duplicación accidental entre dominios diferentes es mucho más barata que el acoplamiento prematuro. Si el departamento fiscal exige un nuevo campo en la factura mañana, el módulo de pedidos no debe verse impactado.
3. Angular: La Era Standalone y DDD Práctico
Con las APIs Standalone como opción por defecto (desde Angular 19) y los NgModules ahora opcionales, Angular se liberó de buena parte de la complejidad burocrática. El patrón moderno se inspira en los tipos de biblioteca de Nx (feature, ui, data-access, util) y en conceptos ligeros de Domain-Driven Design (DDD).
Dividimos la aplicación en cuatro macrocategorías: core, shared (o ui), features y data-access.
Estructura Recomendada para Angular
src/
├─ app/
│ ├─ core/ # Servicios Singleton, interceptors y guards globales
│ │ ├─ auth/
│ │ │ ├─ auth.guard.ts
│ │ │ └─ auth.service.ts
│ │ └─ http/
│ │ └─ error.interceptor.ts
│ │
│ ├─ shared/ # UI "tonta" y genérica (Design System)
│ │ ├─ ui/
│ │ │ ├─ button/
│ │ │ └─ modal/
│ │ └─ utils/
│ │
│ ├─ features/ # Secciones de negocio (Vertical Slices)
│ │ ├─ checkout/
│ │ │ ├─ components/ # Componentes internos y exclusivos del checkout
│ │ │ ├─ services/ # Lógica de presentación del checkout
│ │ │ ├─ checkout.routes.ts # Ruta con Lazy Loading
│ │ │ └─ checkout.component.ts
│ │ │
│ │ └─ catalog/
│ │
│ └─ data-access/ # Comunicación con APIs externas y Estado Global (Signals)
│ ├─ products/
│ │ ├─ products.api.ts
│ │ ├─ products.store.ts # SignalStore / Estado
│ │ └─ products.types.ts
│ └─ user/
- Separation of Concerns (SoC): La carpeta
data-accesscentraliza las llamadas HTTP y la gestión del estado (SignalStore). La capa defeatures/consume estos datos sin saber cómo fueron obtenidos o etiquetados en el caché. Es una excepción deliberada a la colocation de la sección 1: los datos que usan varias features (los productos aparecen en el catálogo y en el checkout) tienen carpeta propia; los de una sola feature se quedan dentro de ella — en Nx,libs/<dominio>/data-access. - Aislamiento de UI: Todo lo que está dentro de
features/checkout/componentses privado. Sifeatures/catalognecesita la misma tarjeta de producto, ese componente se refactoriza y se promueve ashared/ui.
4. React: Feature-Scoped Architecture y Barrel Files
En el ecosistema React (ya sea Next.js con App Router o SPAs con Vite), la libertad arquitectónica suele generar refactorizaciones traumáticas. La estructura más resiliente adopta la metodología Bulletproof React, abandonando carpetas globales saturadas en favor de módulos autosuficientes.
Estructura Recomendada para React
src/
├─ app/ # Enrutamiento (Next.js App Router o configuración de React Router)
│ ├─ (public)/
│ └─ (authenticated)/
│ └─ dashboard/
│ └─ page.tsx # ¡La página actúa únicamente como orquestadora de la Feature!
│
├─ components/ # Componentes globales de UI (Shadcn / Tailwind)
│ ├─ ui/
│ │ ├─ button.tsx
│ │ └─ dialog.tsx
│ └─ layouts/
│
├─ lib/ # Configuraciones de clientes externos (Axios, QueryClient)
│ ├─ axios.ts
│ └─ react-query.ts
│
├─ features/ # El corazón de la aplicación
│ ├─ billing/
│ │ ├─ api/ # Hooks de TanStack Query (useInvoice, usePay)
│ │ ├─ components/ # InvoiceList, PaymentModal
│ │ ├─ hooks/ # Hooks locales de la feature (useTaxCalculator)
│ │ ├─ types/ # Interfaces DTOs restringidas a billing
│ │ └─ index.ts # BARREL FILE: ¡Exporta únicamente lo que es público!
│ │
│ └─ projects/
│
└─ stores/ # Estado global (Zustand, cuando es estrictamente necesario)
└─ use-theme-store.ts
El Poder del Barrel File (index.ts)
Dentro de cada feature React, mantenemos un archivo index.ts en la raíz que actúa como la API pública del módulo. (Aquí nos apartamos de Bulletproof React, que hoy desaconseja los barrel files porque perjudican el tree shaking en Vite; adoptar el barrel es aceptar ese costo a cambio de una frontera explícita.) Si la página de Dashboard necesita renderizar la lista de facturas, importa:
import { InvoiceList } from '@/features/billing';
El barrel, por sí solo, no impide importar archivos internos profundos (como un hook privado useTaxCalculator) por su ruta completa; lo que garantiza el encapsulamiento y la ocultación de información es una regla de lint —import/no-restricted-paths o no-restricted-imports— que prohíbe importar más allá del index.ts.
5. Node.js (NestJS / Express): El Monolito Modular
En el backend, el error fundamental es estructurar el proyecto por capas técnicas en la raíz (/controllers, /models, /repositories). Esto viola el Principio de Responsabilidad Única a nivel de módulo.
Ya sea utilizando NestJS (que nos guía nativamente por este camino) o una configuración limpia con Express/Fastify, debemos diseñar el backend como un Monolito Modular basado en Clean Architecture.
Estructura Recomendada para Node.js
src/
├─ config/ # Variables de entorno, conexión a BD, loggers
├─ shared/ # Middlewares globales, excepciones personalizadas, decorators
│ ├─ errors/
│ └─ middlewares/
│
├─ modules/ # Dominios de negocio (Bounded Contexts)
│ ├─ orders/
│ │ ├─ dto/ # Data Transfer Objects (Zod / Class-Validator)
│ │ │ ├─ create-order.dto.ts
│ │ │ └─ order-response.dto.ts
│ │ │
│ │ ├─ entities/ # Modelos de dominio (sin tipos del ORM)
│ │ │ └─ order.entity.ts
│ │ │
│ │ ├─ repositories/ # Aislamiento de la base de datos (Repository Pattern)
│ │ │ ├─ order.repository.interface.ts
│ │ │ └─ prisma-order.repository.ts
│ │ │
│ │ ├─ use-cases/ # Lógica de negocio pura (O Services en NestJS)
│ │ │ ├─ create-order.usecase.ts
│ │ │ └─ calculate-discount.usecase.ts
│ │ │
│ │ ├─ orders.controller.ts # Capa HTTP / Entrada y salida de datos
│ │ └─ orders.module.ts # Cableado de dependencias (Inyección de Dependencia)
│ │
│ └─ users/
│
└─ main.ts # Punto de entrada (Entrypoint) de la aplicación
Ventajas Estratégicas del Monolito Modular
- Independencia de Infraestructura (DIP): Al aislar los
use-cases(regla de negocio) de losrepositories(acceso a datos), si la empresa decide migrar de MongoDB a PostgreSQL en el futuro, el código a cambiar queda concentrado en/repositories, siempre queentities/no sea el schema del ORM. La regla de negocio permanece intacta; migrar los datos y las diferencias de transacciones entre las dos bases siguen siendo trabajo aparte. - Preparado para Microservicios: Si el módulo de
orderssufre un cuello de botella severo de procesamiento y necesita escalar de forma aislada, ya tiene sus límites definidos. Extraerlo a un microservicio independiente será mucho más barato: el código de dominio se mueve casi intacto, y el trabajo real pasa a ser lo que introduce la red: llamadas que se vuelven remotas, fallas parciales, transacciones que dejan de ser locales y la separación de los datos.
Conclusión
La arquitectura de software no existe para burocratizar el trabajo de la ingeniería con reglas rígidas; su función principal es la gestión de la carga cognitiva.
Cuando organizamos proyectos en React, Angular o Node.js por Features e implementamos los principios SOLID y DRY en la raíz de nuestra estructura, permitimos que cualquier desarrollador pueda entender, modificar y probar una sección específica del sistema sin necesidad de cargar la aplicación entera en su memoria a corto plazo.
La próxima vez que inicies un proyecto o refactorices un sistema legado, abandona la agrupación por sintaxis o tipo de archivo. Empieza a estructurar tu código por los problemas de negocio que resuelve.
Comentarios
Cargando comentarios...