Principios Fundamentales y Estructura del Proyecto
1. Arquitectura del Proyecto​
Regla Principal​
Regla: Todo proyecto NestJS generado o asistido por la IA debe seguir una estructura de directorios clara y modular, basada en funcionalidades (features). La estructura base recomendada es la siguiente:
src/
├── main.ts # Punto de entrada, configuración inicial (pipes globales, etc.)
├── app.module.ts # Módulo raÃz, importa otros módulos y configuración global
├── config/ # Módulo de configuración (variables entorno, validación)
│ ├── configuration.ts
│ └── validation.schema.ts
├── modules/ # Directorio principal para módulos de funcionalidades
│ ├── [feature1]/ # Módulo para una funcionalidad especÃfica (ej. users, products)
│ │ ├── controllers/
│ │ ├── services/
│ │ ├── dtos/
│ │ ├── entities/ # (Opcional, según ORM)
│ │ ├── repositories/ # (Opcional, patrón repositorio)
│ │ └── [feature1].module.ts
│ └── [feature2]/
│ └── ...
├── common/ # Código transversal reutilizable (no especÃfico de un módulo)
│ ├── decorators/
│ ├── exceptions/
│ ├── filters/
│ ├── guards/
│ ├── interceptors/
│ ├── pipes/
│ └── utils/
├── database/ # Configuración ORM, migraciones, seeds (si aplica)
│ ├── migrations/
│ └── seeds/
├── tests/ # Pruebas (unitarias, integración, E2E)
│ ├── unit/
│ └── e2e/
└── assets/ # Archivos estáticosLa IA debe colocar los archivos generados (controladores, servicios, DTOs, etc.) dentro del directorio del módulo de funcionalidad correspondiente (
src/modules/[feature]/). El código reutilizable que no pertenezca a una única funcionalidad debe ubicarse ensrc/common/.
Contexto y Justificación​
Esta estructura es obligatoria para garantizar la escalabilidad, mantenibilidad y claridad del proyecto. Agrupar por funcionalidad (módulo) mejora la cohesión y reduce el acoplamiento entre diferentes partes de la aplicación. Facilita la navegación, el testing independiente de módulos y la potencial división en microservicios. La IA debe generar código que se integre naturalmente en esta estructura.
Ejemplos y Contraejemplos​
- Correcto: Crear un nuevo controlador
ProductsControllerensrc/modules/products/controllers/products.controller.tsy su servicio asociado ensrc/modules/products/services/products.service.ts. Crear un pipe de validación reutilizable ensrc/common/pipes/custom-validation.pipe.ts. - Incorrecto: Colocar
ProductsControllerdirectamente ensrc/controllers/. Crear un servicioAuthServicedentro del móduloUsersModule. Poner utilidades genéricas dentro de un módulo especÃfico.
Cuándo Aplicar​
Siempre, desde el inicio del proyecto NestJS.
Cuándo Evitar o Flexibilizar​
Nunca. Esta estructura base es fundamental. Se pueden añadir otros directorios de alto nivel si están justificados (ej.
libs/para librerÃas internas), pero la organización central por módulos funcionales debe mantenerse.
2. Organización de Módulos​
Regla Principal​
Regla: Cada funcionalidad principal de la aplicación debe encapsularse en su propio Módulo NestJS (
@Module).
- Un módulo debe declarar sus propios
controllers,providers(servicios, repositorios), e importar otros módulos si necesita sus providers exportados.- Los
providers(especialmente servicios) deben ser privados al módulo por defecto. Solo se deben exportar (exports: [MyService]) si es estrictamente necesario que otro módulo los consuma directamente.- La comunicación entre módulos debe realizarse preferentemente a través de llamadas a controladores (si aplica) o mediante los servicios exportados del módulo requerido, importando dicho módulo en el
importsarray. Se debe evitar la dependencia circular entre módulos.- La lógica compartida y transversal (autenticación, logging, utilidades genéricas) debe residir en módulos dentro de
src/common/o en módulos dedicados importados enAppModuleo donde se necesiten.
Contexto y Justificación​
La modularidad es un pilar de NestJS. Encapsular funcionalidades en módulos promueve el bajo acoplamiento y la alta cohesión. Facilita la organización del código, el testing aislado, la reutilización y la posibilidad de implementar carga perezosa (
lazy loading) o refactorizar hacia microservicios. Limitar las exportaciones reduce dependencias no deseadas.
Ejemplos y Contraejemplos​
- Correcto:
// src/modules/orders/orders.module.ts
import { Module } from '@nestjs/common';
import { ProductsModule } from '../products/products.module'; // Importa ProductsModule si necesita ProductsService
import { OrdersController } from './controllers/orders.controller';
import { OrdersService } from './services/orders.service';
@Module({
imports: [ProductsModule], // Necesita un servicio exportado por ProductsModule
controllers: [OrdersController],
providers: [OrdersService],
// No exporta OrdersService si nadie más lo necesita directamente
})
export class OrdersModule {}
// src/modules/products/products.module.ts
import { Module } from '@nestjs/common';
import { ProductsController } from './controllers/products.controller';
import { ProductsService } from './services/products.service';
@Module({
controllers: [ProductsController],
providers: [ProductsService],
exports: [ProductsService] // Exporta ProductsService para que OrdersModule lo use
})
export class ProductsModule {} - Incorrecto: Un
AppModulegigante que declara todos los controladores y servicios. Módulos importándose mutuamente creando dependencias circulares. UnUsersServiceimportando directamente unOrdersRepository.
Cuándo Aplicar​
Siempre. Para cada conjunto lógico de funcionalidades relacionadas.
Cuándo Evitar o Flexibilizar​
Para aplicaciones extremadamente simples (ej. un solo endpoint), un único
AppModulepodrÃa bastar, pero la estructura modular es la práctica estándar y recomendada incluso para proyectos pequeños. La decisión de exportar un servicio debe ser consciente y justificada.
3. Aplicación de Principios SOLID​
Regla Principal​
Regla: El código generado o asistido por la IA en NestJS debe adherirse a los principios SOLID para promover un diseño robusto, mantenible y testeable.
- S (Single Responsibility Principle - SRP): Cada clase (controlador, servicio, provider) debe tener una única responsabilidad bien definida. Un servicio no debe manejar lógica de múltiples dominios no relacionados.
- O (Open/Closed Principle - OCP): Las entidades de software deben estar abiertas para extensión, pero cerradas para modificación. Utilizar composición, inyección de dependencias y patrones (como Strategy) para añadir nueva funcionalidad sin alterar el código existente estable.
- L (Liskov Substitution Principle - LSP): Las subclases (si se usan) deben ser sustituibles por sus clases base sin alterar la correctitud del programa. En NestJS, esto se aplica más comúnmente al usar interfaces (
interface) para definir contratos que los servicios implementan.- I (Interface Segregation Principle - ISP): Los clientes no deben ser forzados a depender de interfaces que no utilizan. Crear interfaces especÃficas y cohesivas en lugar de interfaces monolÃticas. Por ejemplo, definir
interface IEmailServiceseparada deinterface ISmsService.- D (Dependency Inversion Principle - DIP): Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones (interfaces). Las abstracciones no deben depender de detalles; los detalles deben depender de abstracciones. Utilizar intensivamente la Inyección de Dependencias (DI) de NestJS, inyectando abstracciones (interfaces o tokens) en lugar de clases concretas siempre que se busque desacoplamiento.
Contexto y Justificación​
Los principios SOLID son fundamentales para crear software de alta calidad. Su aplicación en NestJS, facilitada por su sistema de módulos y DI, conduce a código más flexible, menos propenso a errores, más fácil de testear (mediante mocks y stubs) y más adaptable a cambios futuros.
Ejemplos y Contraejemplos​
- Correcto (SRP, DIP):
// --- Servicio con única responsabilidad (SRP) ---
@Injectable()
export class ReportGeneratorService {
generatePdfReport(data: any): Buffer { /* ... lógica PDF ... */ }
}
@Injectable()
export class EmailNotifierService {
sendReportByEmail(report: Buffer, recipient: string) { /* ... lógica email ... */ }
}
// --- Uso de DIP con interfaces y DI ---
export const USER_REPOSITORY_TOKEN = Symbol('IUserRepository'); // Token de inyección
export interface IUserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<User>;
}
@Injectable()
export class UserService {
// Depende de la abstracción (IUserRepository)
constructor(
@Inject(USER_REPOSITORY_TOKEN) private readonly userRepository: IUserRepository
) {}
async getUser(id: string): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) throw new NotFoundException();
return user;
}
}
// Implementación concreta (detalle)
@Injectable()
export class PrismaUserRepository implements IUserRepository {
constructor(private readonly prisma: PrismaClient) {}
async findById(id: string): Promise<User | null> { /* ... impl con Prisma ... */ }
async save(user: User): Promise<User> { /* ... impl con Prisma ... */ }
}
// En el módulo correspondiente:
// providers: [ UserService, { provide: USER_REPOSITORY_TOKEN, useClass: PrismaUserRepository } ] - Incorrecto (Violación SRP): Un
UserServiceque también envÃa emails de bienvenida, genera reportes PDF y procesa pagos. - Incorrecto (Violación DIP):
@Injectable()
export class BadUserService {
private readonly userRepository: PrismaUserRepository; // Dependencia de la clase concreta
constructor(prisma: PrismaClient) {
this.userRepository = new PrismaUserRepository(prisma); // Instanciación directa
}
// ...
}
Cuándo Aplicar​
Siempre. Estos principios deben guiar el diseño de clases y la interacción entre módulos y servicios.
Cuándo Evitar o Flexibilizar​
No se deben ignorar los principios SOLID. La aplicación puede ser más o menos estricta según la complejidad, pero la intención general (desacoplamiento, cohesión, testeabilidad) debe mantenerse. La inversión de dependencias con interfaces/tokens es más crucial en capas de acceso a datos o servicios externos.