Saltar al contenido principal

Componentes Core

4. Regla sobre Controladores

Regla Principal

Regla: Los Controladores (@Controller) deben ser responsables únicamente de:

  1. Recibir las peticiones HTTP entrantes.
  2. Validar y extraer datos de la petición (usando DTOs, @Param, @Query, @Body, etc.).
  3. Delegar toda la lógica de negocio al Servicio correspondiente.
  4. Formatear la respuesta HTTP (usando códigos de estado apropiados (@HttpCode), serialización de respuesta si es necesaria). Está prohibido implementar lógica de negocio, acceso a datos o interacción directa con librerías externas (excepto las relacionadas con HTTP/framework) dentro de un Controlador. Se deben utilizar los decoradores de NestJS (@Get, @Post, @UseGuards, @UseInterceptors, @ApiTags, etc.) para definir rutas, aplicar guards/interceptors y documentar la API (Swagger).

Contexto y Justificación

Los controladores actúan como la capa de entrada de la API. Mantenerlos "delgados" (thin controllers) y centrados en el manejo de HTTP asegura una clara separación de responsabilidades (SRP). Delegar la lógica a los servicios facilita el testing unitario de dicha lógica (sin necesidad de simular peticiones HTTP) y mejora la mantenibilidad y reutilización del código de negocio.

Ejemplos y Contraejemplos

  • Correcto:
    import { Controller, Get, Post, Body, Param, Inject, UseGuards, HttpCode, ParseUUIDPipe } from '@nestjs/common';
    import { CreateItemDto } from './dtos/create-item.dto';
    import { IItemService } from './interfaces/item.service.interface'; // Depende de abstracción
    import { JwtAuthGuard } from '@app/common/guards/jwt-auth.guard'; // Reutilizable
    import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';

    @ApiTags('items')
    @UseGuards(JwtAuthGuard) // Aplica guard a todo el controlador
    @Controller('items')
    export class ItemsController {
    // Inyecta la interfaz del servicio (DIP)
    constructor(@Inject('IItemService') private readonly itemService: IItemService) {}

    @Post()
    @HttpCode(201) // Código de estado explícito para POST exitoso
    @ApiOperation({ summary: 'Crea un nuevo ítem' })
    @ApiResponse({ status: 201, description: 'Ítem creado exitosamente.' })
    @ApiResponse({ status: 400, description: 'Entrada inválida.' })
    create(@Body() createItemDto: CreateItemDto) { // Usa DTO para validación automática
    // Delega la creación al servicio
    return this.itemService.createItem(createItemDto);
    }

    @Get(':id')
    @ApiOperation({ summary: 'Obtiene un ítem por ID' })
    @ApiResponse({ status: 200, description: 'Ítem encontrado.' })
    @ApiResponse({ status: 404, description: 'Ítem no encontrado.' })
    findOne(@Param('id', ParseUUIDPipe) id: string) { // Usa Pipe para validar/transformar parámetro
    // Delega la búsqueda al servicio
    return this.itemService.findItemById(id);
    }
    }
  • Incorrecto:
    import { Controller, Post, Body } from '@nestjs/common';
    import { PrismaClient } from '@prisma/client'; // ¡INCORRECTO: Acceso directo a BD!

    @Controller('bad-items')
    export class BadItemsController {
    private prisma = new PrismaClient(); // ¡INCORRECTO: Instanciación directa!

    @Post()
    async create(@Body() body: any) { // ¡INCORRECTO: Sin DTO, tipo any!
    // ¡INCORRECTO: Lógica de negocio y acceso a datos en el controlador!
    if (!body.name || body.price <= 0) {
    throw new BadRequestException('Nombre y precio positivo requeridos.');
    }
    const newItem = await this.prisma.item.create({
    data: {
    name: body.name,
    price: body.price,
    },
    });
    // ¡INCORRECTO: Lógica adicional (ej. enviar evento) aquí!
    console.log('Item creado', newItem.id);
    return newItem; // Falta código de estado explícito (debería ser 201)
    }
    }

Cuándo Aplicar

Siempre, para todos los endpoints de la API REST.

Cuándo Evitar o Flexibilizar

Nunca. La separación entre controlador y servicio es fundamental en NestJS.

5. Regla sobre Servicios

Regla Principal

Regla: Los Servicios (@Injectable) deben contener la lógica de negocio principal de la aplicación.

  1. Deben orquestar las operaciones, interactuando con repositorios (o directamente con el ORM/base de datos si no se usa el patrón Repository), otros servicios o APIs externas.
  2. Deben ser agnósticos al protocolo de transporte (HTTP, WebSockets, gRPC). No deben depender de objetos como Request o Response de @nestjs/common.
  3. Deben lanzar excepciones específicas del dominio o excepciones de NestJS (NotFoundException, BadRequestException, etc.) cuando las operaciones fallan o las condiciones de negocio no se cumplen.
  4. Deben definirse como providers dentro de un módulo NestJS y ser inyectados en controladores u otros servicios usando la Inyección de Dependencias.
  5. Para lógica compleja, deben dividirse en métodos más pequeños y cohesivos siguiendo el principio de Responsabilidad Única (SRP).

Contexto y Justificación

Los servicios son el corazón de la lógica de negocio. Mantenerlos desacoplados del framework y del protocolo HTTP los hace reutilizables y mucho más fáciles de testear unitariamente. Centralizar la lógica de negocio aquí mejora la organización y mantenibilidad de la aplicación.

Ejemplos y Contraejemplos

  • Correcto:
    import { Injectable, Inject, NotFoundException, InternalServerErrorException } from '@nestjs/common';
    import { CreateUserDto } from './dtos/create-user.dto';
    import { User } from './entities/user.entity';
    import { IUserRepository } from './interfaces/user.repository.interface'; // Interfaz Repositorio
    import { IEmailService } from '../common/interfaces/email.service.interface'; // Interfaz Servicio Externo
    import { hash } from 'bcrypt'; // Librería externa para hashing

    @Injectable()
    export class UsersService {
    constructor(
    // Inyecta dependencias (abstracciones)
    @Inject('IUserRepository') private readonly userRepository: IUserRepository,
    @Inject('IEmailService') private readonly emailService: IEmailService,
    ) {}

    async createUser(createUserDto: CreateUserDto): Promise<Omit<User, 'password'>> {
    try {
    // Lógica de negocio: hashear contraseña
    const hashedPassword = await hash(createUserDto.password, 10);

    // Lógica de negocio: Crear usuario vía repositorio
    const newUser = await this.userRepository.create({
    ...createUserDto,
    password: hashedPassword,
    });

    // Lógica de negocio: Enviar email de bienvenida (delegado)
    await this.emailService.sendWelcomeEmail(newUser.email, newUser.name);

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { password, ...result } = newUser; // Omite la contraseña en la respuesta
    return result;
    } catch (error) {
    // Manejo de errores específico del servicio
    console.error('Error creando usuario:', error);
    throw new InternalServerErrorException('No se pudo crear el usuario.');
    }
    }

    async findUserById(id: string): Promise<User> {
    const user = await this.userRepository.findById(id);
    if (!user) {
    // Lanza excepción de NestJS
    throw new NotFoundException(`Usuario con ID "${id}" no encontrado.`);
    }
    return user;
    }
    }
  • Incorrecto:
    import { Injectable, Req, Res } from '@nestjs/common';
    import { Request, Response } from 'express'; // ¡INCORRECTO: Dependencia de Express!
    import { Database } from 'some-db-library'; // ¡INCORRECTO: Acoplamiento fuerte!

    @Injectable()
    export class BadUsersService {
    private db = new Database(); // ¡INCORRECTO: Instanciación directa!

    // ¡INCORRECTO: Dependencia directa de Req/Res!
    async getUserAndRespond(userId: string, @Req() req: Request, @Res() res: Response) {
    const user = await this.db.findUser(userId);
    if (!user) {
    return res.status(404).send('Not found'); // ¡INCORRECTO: Manejo de respuesta HTTP!
    }
    // ¡INCORRECTO: Lógica compleja mezclada!
    const report = this.generateReport(user);
    this.sendEmail(user.email, report);
    return res.status(200).json(user);
    }

    private generateReport(user: any) { /* ... */ }
    private sendEmail(email: string, report: any) { /* ... */ }
    }

Cuándo Aplicar

Siempre, para encapsular toda la lógica de negocio asociada a un módulo o entidad.

Cuándo Evitar o Flexibilizar

Nunca se debe poner lógica de negocio significativa fuera de los servicios. Para operaciones CRUD muy simples, el servicio puede ser delgado, pero aún así debe existir para mantener la estructura.

6. Regla sobre DTOs y Validaciones

Regla Principal

Regla: Toda entrada de datos proveniente del exterior (ej. cuerpo de petición HTTP, query params) debe ser validada usando Data Transfer Objects (DTOs) y el ValidationPipe global.

  1. Se deben crear clases DTO específicas para cada operación que reciba datos (ej. CreateUserDto, UpdateUserDto, LoginDto, PaginationQueryDto).
  2. Las propiedades de las clases DTO deben decorarse con validadores de class-validator (@IsString, @IsEmail, @IsInt, @Min, @Max, @IsOptional, @ValidateNested, etc.) para definir las reglas de validación.
  3. Se debe habilitar el ValidationPipe globalmente en main.ts con las opciones whitelist: true y forbidNonWhitelisted: true para asegurar que solo las propiedades definidas en el DTO sean aceptadas y para rechazar propiedades inesperadas.
  4. Se deben usar transformadores de class-transformer (@Transform, @Type) dentro de los DTOs cuando sea necesario convertir tipos de datos (ej. string a number en query params) o realizar transformaciones simples.
  5. Está prohibido usar tipos genéricos como any o object en los decoradores @Body(), @Query(), @Param() de los controladores. Siempre se debe usar una clase DTO específica.

Contexto y Justificación

Validar las entradas es crucial para la seguridad (prevenir inyecciones, datos malformados) y la robustez de la aplicación. Usar DTOs con class-validator y class-transformer, junto con el ValidationPipe global, proporciona una forma declarativa, reutilizable y centralizada de manejar la validación y transformación de datos, manteniendo los controladores y servicios limpios de lógica de validación manual.

Ejemplos y Contraejemplos

  • Correcto (DTO y Configuración Global):
    // --- src/modules/tasks/dtos/create-task.dto.ts ---
    import { IsString, IsNotEmpty, IsEnum, IsOptional, MaxLength } from 'class-validator';
    import { TaskStatus } from '../entities/task.entity'; // Suponiendo un enum TaskStatus

    export class CreateTaskDto {
    @IsString()
    @IsNotEmpty()
    @MaxLength(100)
    title: string;

    @IsString()
    @IsOptional()
    description?: string;

    @IsEnum(TaskStatus) // Valida contra un enum
    @IsOptional()
    status: TaskStatus = TaskStatus.OPEN; // Valor por defecto
    }

    // --- src/modules/tasks/dtos/get-tasks-filter.dto.ts (Query Params) ---
    import { IsOptional, IsEnum, IsString, IsInt } from 'class-validator';
    import { Type } from 'class-transformer';
    import { TaskStatus } from '../entities/task.entity';

    export class GetTasksFilterDto {
    @IsOptional()
    @IsEnum(TaskStatus)
    status?: TaskStatus;

    @IsOptional()
    @IsString()
    search?: string;

    @IsOptional()
    @Type(() => Number) // Transforma string a number
    @IsInt()
    limit?: number;
    }

    // --- src/main.ts ---
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { ValidationPipe } from '@nestjs/common';

    async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    app.useGlobalPipes(
    new ValidationPipe({
    whitelist: true, // Elimina propiedades no definidas en el DTO
    forbidNonWhitelisted: true, // Lanza error si hay propiedades no definidas
    transform: true, // Habilita la transformación automática (ej. @Type)
    transformOptions: {
    enableImplicitConversion: true, // Permite conversión implícita para primitivos en query/params
    },
    }),
    );
    await app.listen(3000);
    }
    bootstrap();

    // --- En el controlador ---
    import { Controller, Post, Body, Get, Query } from '@nestjs/common';
    import { CreateTaskDto } from './dtos/create-task.dto';
    import { GetTasksFilterDto } from './dtos/get-tasks-filter.dto';

    @Controller('tasks')
    export class TasksController {
    // ... inyectar servicio ...

    @Post()
    createTask(@Body() createTaskDto: CreateTaskDto) { // Usa el DTO para el Body
    // createTaskDto ya está validado y transformado
    // ... llamar al servicio ...
    }

    @Get()
    getTasks(@Query() filterDto: GetTasksFilterDto) { // Usa el DTO para Query Params
    // filterDto ya está validado y transformado
    // ... llamar al servicio ...
    }
    }
  • Incorrecto:
    import { Controller, Post, Body, Get, Query } from '@nestjs/common';

    @Controller('bad-tasks')
    export class BadTasksController {
    @Post()
    createTask(@Body() body: any) { // ¡INCORRECTO: any!
    // ¡INCORRECTO: Validación manual en el controlador!
    if (!body.title || typeof body.title !== 'string') {
    throw new BadRequestException('Título es requerido');
    }
    // ...
    }

    @Get()
    getTasks(@Query('status') status: string, @Query('limit') limit: string) { // ¡INCORRECTO: Primitivos!
    // ¡INCORRECTO: Conversión y validación manual!
    const numericLimit = limit ? parseInt(limit, 10) : undefined;
    if (isNaN(numericLimit)) {
    throw new BadRequestException('Limit debe ser un número');
    }
    // ...
    }
    }
    // Ausencia de ValidationPipe global en main.ts

Cuándo Aplicar

Siempre, para todos los datos que entran a la aplicación desde fuentes externas a través de controladores.

Cuándo Evitar o Flexibilizar

Nunca. La validación de entradas mediante DTOs es una práctica de seguridad y robustez indispensable en NestJS.