Saltar al contenido principal

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áticos

La 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 en src/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 ProductsController en src/modules/products/controllers/products.controller.ts y su servicio asociado en src/modules/products/services/products.service.ts. Crear un pipe de validación reutilizable en src/common/pipes/custom-validation.pipe.ts.
  • Incorrecto: Colocar ProductsController directamente en src/controllers/. Crear un servicio AuthService dentro del módulo UsersModule. 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).

  1. Un módulo debe declarar sus propios controllers, providers (servicios, repositorios), e importar otros módulos si necesita sus providers exportados.
  2. 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.
  3. 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 imports array. Se debe evitar la dependencia circular entre módulos.
  4. 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 en AppModule o 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 AppModule gigante que declara todos los controladores y servicios. Módulos importándose mutuamente creando dependencias circulares. Un UsersService importando directamente un OrdersRepository.

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 AppModule podrí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.

  1. 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.
  2. 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.
  3. 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.
  4. 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 IEmailService separada de interface ISmsService.
  5. 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 UserService que 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.