Componentes Core
4. Regla sobre Controladores
Regla Principal
Regla: Los Controladores (
@Controller) deben ser responsables únicamente de:
- Recibir las peticiones HTTP entrantes.
- Validar y extraer datos de la petición (usando DTOs,
@Param,@Query,@Body, etc.).- Delegar toda la lógica de negocio al Servicio correspondiente.
- 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.
- 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.
- Deben ser agnósticos al protocolo de transporte (HTTP, WebSockets, gRPC). No deben depender de objetos como
RequestoResponsede@nestjs/common.- 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.- Deben definirse como
providersdentro de un módulo NestJS y ser inyectados en controladores u otros servicios usando la Inyección de Dependencias.- 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
ValidationPipeglobal.
- Se deben crear clases DTO específicas para cada operación que reciba datos (ej.
CreateUserDto,UpdateUserDto,LoginDto,PaginationQueryDto).- 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.- Se debe habilitar el
ValidationPipeglobalmente enmain.tscon las opcioneswhitelist: trueyforbidNonWhitelisted: truepara asegurar que solo las propiedades definidas en el DTO sean aceptadas y para rechazar propiedades inesperadas.- 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.- Está prohibido usar tipos genéricos como
anyoobjecten 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-validatoryclass-transformer, junto con elValidationPipeglobal, 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.