Optimizando la Estructura de Carpetas en Proyectos Angular
Introducción
La organización de archivos y carpetas puede parecer un aspecto secundario al desarrollar aplicaciones Angular, pero en realidad es una decisión arquitectónica que tiene un profundo impacto en la mantenibilidad, escalabilidad y comprensión del proyecto. En este artículo, exploraremos diferentes enfoques para estructurar proyectos Angular, desde el más simple hasta el más orientado al dominio de negocio.
Cuando iniciamos un proyecto con Angular CLI, se nos proporciona una estructura predeterminada. Sin embargo, a medida que la aplicación crece, esta estructura inicial puede resultar insuficiente. ¿Deberíamos seguir las convenciones del framework o adaptar la estructura para reflejar mejor nuestro dominio de negocio?
La diferencia entre Scaffolding y Estructura de Carpetas
Antes de profundizar, es importante distinguir entre dos conceptos relacionados pero diferentes:
Scaffolding se refiere al proceso automático de generación de código y estructura inicial utilizando herramientas como Angular CLI. Comandos como ng new
o ng generate component
crean automáticamente archivos con código boilerplate y los registran en los módulos correspondientes.
Estructura de carpetas es la organización final de archivos y directorios en el proyecto, que puede ser el resultado del scaffolding inicial, pero también de decisiones conscientes sobre cómo organizar el código según las necesidades específicas del proyecto.
El scaffolding es un proceso que ocurre en momentos específicos durante el desarrollo, mientras que la estructura de carpetas evoluciona continuamente y está más bajo el control del equipo de desarrollo.
Tres niveles de estructura en proyectos Angular
Podemos identificar tres enfoques principales para organizar nuestros proyectos Angular, cada uno con diferentes niveles de complejidad y beneficios:
Nivel 1: Estructura por tipo de archivo
Esta es la estructura más básica, generalmente creada por defecto con Angular CLI:
└── src/
├── app/
│ ├── components/
│ ├── services/
│ ├── models/
│ ├── pipes/
│ ├── directives/
│ └── guards/
├── assets/
├── environments/
└── ...
Ventajas:
- Familiar para desarrolladores Angular
- Sigue las convenciones del framework
- Rápida de implementar
Desventajas:
- No comunica el propósito de negocio
- Se vuelve difícil de mantener a medida que crece
- Los módulos relacionados están dispersos
Casos de uso ideales:
- Prototipos (POC - Prueba de Concepto)
- Productos Mínimos Viables (MVP)
- Proyectos a corto plazo
- Equipos muy pequeños (1-2 desarrolladores)
Esta estructura es perfecta cuando necesitamos validar rápidamente una idea o concepto, sin preocuparnos demasiado por la escalabilidad a largo plazo.
Nivel 2: Estructura por características
Este enfoque agrupa el código primero por características de la aplicación:
└── src/
├── app/
│ ├── core/ # Servicios singleton, guards globales, etc.
│ │ ├── services/
│ │ ├── guards/
│ │ └── interceptors/
│ ├── shared/ # Componentes, pipes, directivas compartidas
│ │ ├── components/
│ │ ├── directives/
│ │ └── pipes/
│ ├── features/ # Características de la aplicación
│ │ ├── auth/
│ │ │ ├── components/
│ │ │ ├── services/
│ │ │ └── auth.module.ts
│ │ ├── payment/
│ │ │ ├── components/
│ │ │ ├── services/
│ │ │ └── payment.module.ts
│ │ └── employees/
│ │ ├── components/
│ │ ├── services/
│ │ └── employees.module.ts
│ └── app.module.ts
├── assets/
├── environments/
└── ...
Ventajas:
- Organiza el código por características de la aplicación
- Utiliza módulos Angular para encapsular funcionalidades
- Facilita el trabajo paralelo entre equipos
- Mantiene separados los componentes compartidos
Desventajas:
- Todavía mantiene cierta orientación técnica en lugar de enfoque de dominio
- Puede ser difícil identificar límites entre características
Casos de uso ideales:
- Proyectos de tamaño medio (3-10 desarrolladores)
- Aplicaciones con expectativa de crecimiento moderado
- Equipos con rotación de personal
Esta estructura es probablemente la más utilizada en proyectos Angular de tamaño medio, ya que ofrece un buen equilibrio entre organización y facilidad de comprensión.
Nivel 3: Estructura con Arquitectura Gritante (Screaming Architecture)
Este enfoque está inspirado en el concepto de "Arquitectura Gritante" propuesto por Robert C. Martin, donde la estructura del proyecto debería "gritar" su propósito de negocio:
└── src/
├── app/
│ ├── core/ # Infraestructura de la aplicación
│ │ ├── infrastructure/ # Implementaciones técnicas
│ │ │ ├── http/
│ │ │ ├── storage/
│ │ │ └── auth/
│ │ └── ui/ # Componentes UI base
│ │ ├── layout/
│ │ └── components/
│ ├── domains/ # Dominios de negocio (módulos)
│ │ ├── hr-management/ # Dominio: Gestión de RRHH
│ │ │ ├── employee-directory/ # Caso de uso
│ │ │ │ ├── components/
│ │ │ │ ├── services/
│ │ │ │ └── models/
│ │ │ ├── payroll/ # Caso de uso
│ │ │ └── hr-management.module.ts
│ │ ├── sales/ # Dominio: Ventas
│ │ │ ├── product-catalog/ # Caso de uso
│ │ │ ├── checkout/ # Caso de uso
│ │ │ └── sales.module.ts
│ │ └── customer-support/ # Dominio: Soporte al Cliente
│ │ ├── ticket-management/ # Caso de uso
│ │ ├── knowledge-base/ # Caso de uso
│ │ └── customer-support.module.ts
│ ├── shared/ # Componentes compartidos entre dominios
│ └── app.module.ts
├── assets/
├── environments/
└── ...
Ventajas:
- La estructura comunica claramente el propósito de negocio
- Organiza el código por dominios y casos de uso
- Facilita la comprensión del negocio para nuevos desarrolladores
- Cada dominio puede evolucionar independientemente
- Promueve la separación de responsabilidades
Desventajas:
- Mayor complejidad inicial
- Requiere buen conocimiento del dominio de negocio
- Puede ser excesivo para aplicaciones pequeñas
Casos de uso ideales:
- Proyectos grandes y complejos
- Numerosos equipos (10+ desarrolladores)
- Aplicaciones de larga vida
- Sistemas con dominios de negocio bien definidos
¿Qué es la Arquitectura Gritante?
El concepto de "Arquitectura Gritante" (Screaming Architecture) fue propuesto por Robert C. Martin (Uncle Bob) en 2011. La idea central es que la arquitectura de un proyecto debería comunicar claramente su propósito y dominio de negocio, no la tecnología o framework utilizado.
La analogía que ofrece es clara: al mirar los planos de un arquitecto, identificas inmediatamente si es una casa, un aeropuerto o un centro comercial, sin necesidad de ver qué materiales se utilizarán. De manera similar, al abrir un proyecto de software, deberíamos poder identificar inmediatamente qué hace (una aplicación de podcasts, un sistema de venta de entradas, etc.), no solo qué tecnología utiliza (Angular, React, etc.).
En el contexto de Angular, esto significa:
- El framework es solo una herramienta, no el fundamento de nuestra arquitectura
- La estructura debe reflejar conceptos de dominio, no convenciones del framework
- Las decisiones críticas deben basarse en necesidades de negocio, no en características del framework
Implementación práctica en Angular
Veamos cómo podemos implementar estas estructuras en Angular, enfocándonos especialmente en el enfoque de Arquitectura Gritante:
1. Usando módulos para dominios
Angular tiene un sistema de módulos que encaja perfectamente con el concepto de dominios:
// domains/hr-management/hr-management.module.ts
@NgModule({
declarations: [...],
imports: [
CommonModule,
SharedModule,
RouterModule.forChild([
{ path: 'employees', component: EmployeeListComponent },
{ path: 'payroll', component: PayrollComponent }
])
],
providers: [EmployeeService, PayrollService]
})
export class HrManagementModule { }
2. Estado por dominio (con NGRX/NGXS)
Si utilizas gestión de estado, cada dominio puede tener su propio estado:
└── domains/
└── hr-management/
├── store/
│ ├── actions/
│ ├── reducers/
│ ├── effects/
│ └── selectors/
└── ...
3. Patrón Barrel
Facilita las importaciones utilizando el patrón barrel:
// domains/hr-management/index.ts
export * from './models';
export * from './services';
export * from './components';
4. Carga perezosa
Aprovecha la carga perezosa (lazy loading) de Angular para cargar dominios solo cuando sea necesario:
// En app-routing.module.ts
const routes: Routes = [
{
path: 'hr',
loadChildren: () => import('./domains/hr-management/hr-management.module')
.then(m => m.HrManagementModule)
},
{
path: 'sales',
loadChildren: () => import('./domains/sales/sales.module')
.then(m => m.SalesModule)
}
];
Ejemplo de transformación
Para ilustrar mejor el concepto, veamos cómo podríamos transformar una estructura tradicional en una basada en Arquitectura Gritante:
Antes (enfoque basado en tipos)
app/
├── components/
│ ├── product-list.component.ts
│ ├── cart.component.ts
│ └── checkout-form.component.ts
├── services/
│ ├── product.service.ts
│ └── cart.service.ts
└── models/
├── product.model.ts
└── cart-item.model.ts
Después (enfoque de Arquitectura Gritante)
app/
└── domains/
├── catalog/
│ ├── components/
│ │ └── product-list.component.ts
│ ├── services/
│ │ └── product.service.ts
│ └── models/
│ └── product.model.ts
└── shopping/
├── components/
│ ├── cart.component.ts
│ └── checkout-form.component.ts
├── services/
│ └── cart.service.ts
└── models/
└── cart-item.model.ts
Consideraciones prácticas
¿Cuándo usar cada enfoque?
No existe una estructura "correcta" universal. La elección depende de varios factores:
Para proyectos pequeños (1-3 desarrolladores):
- El Nivel 1 es ideal para POCs y MVPs donde la prioridad es validar conceptos rápidamente
- El Nivel 2 es adecuado si se espera un crecimiento moderado
Para proyectos medianos (4-10 desarrolladores):
- El Nivel 2 suele ser el más equilibrado
- Considera el Nivel 3 si el dominio es complejo y está bien definido
- Implementa carga perezosa para módulos/características
- Evalúa usar NgRx/NGXS con estado separado por dominio
Para proyectos grandes (10+ desarrolladores):
- El Nivel 3 con Arquitectura Gritante ofrece los mejores beneficios a largo plazo
- Considera dividir en múltiples aplicaciones o microfrontends
- Usa bibliotecas compartidas para código común entre dominios
Migración gradual
No es necesario hacer una refactorización completa inmediatamente. Puedes adoptar un enfoque gradual:
- Identifica los principales dominios de tu aplicación
- Comienza reorganizando un dominio a la vez
- Utiliza el patrón barrel para minimizar cambios en importaciones
- Implementa carga perezosa para reducir el impacto en rendimiento durante la migración
Aquí hay una versión más humanizada de esa sección de resumen:
Para resumir
Al final del día, lo que hace que una estructura de carpetas sea realmente "buena" no es seguir patrones de moda o reglas rígidas, sino lo que funciona para tu equipo en el mundo real. La mejor estructura es aquella que hace que tus desarrolladores sonrían cuando navegan por el código, no que maldigan en voz baja.
Piensa en tu estructura de carpetas como una parte viva y respirante de tu aplicación. Debe crecer y evolucionar junto con tu proyecto, adaptándose a nuevos desafíos e ideas que tu equipo adquiere en el camino. Algunas de las estructuras más efectivas que he visto vinieron de equipos que no tenían miedo de decir "Esto ya no funciona para nosotros" y hacer cambios reflexivos.
Publicaciones más recientes