---
name: nestjs-experto
description: >
  NestJS con TypeScript: módulos, controladores, servicios, DTOs con class-validator,
  guards JWT, interceptors, TypeORM y Prisma. Cargar cuando se implementen APIs
  NestJS, módulos con inyección de dependencias, autenticación JWT/Passport,
  endpoints REST o GraphQL con NestJS, o configuración de pipes y guards.
version: "1.0.0"
herramientasPermitidas: [Read]
exclusiones:
  - "No cargar para APIs Express o Fastify sin NestJS — la DI, los decoradores y el sistema de módulos son exclusivos de NestJS; para Node.js sin framework opinionado cargar `node-experto`."
  - "No cargar para el frontend Next.js aunque comparten TypeScript — Next.js y NestJS son backends distintos con modelos de rendering y lifecycle diferentes; cargar `nextjs-experto`."
  - "No cargar para la configuración de TypeORM o Prisma independientemente de NestJS — si la pregunta es sobre migraciones o queries del ORM en un proyecto no-NestJS, cargar el skill del ORM directamente."
  - "No cargar para errores de compilación TypeScript en proyectos NestJS — esos son errores TS genéricos; cargar `build-errors-typescript` o `typescript-diagnosticos`."
evolvable: true  # default para skill estandar
---
# NestJS Experto — APIs TypeScript de Producción

## Cuándo NO cargar

- El proyecto usa Express o Fastify sin NestJS — para Node.js sin framework opinionado cargar `node-experto`.
- La app es Next.js frontend — NestJS y Next.js son backends distintos; cargar `nextjs-experto`.
- Los errores son de tsconfig o tsc — cargar `build-errors-typescript`.

## Estructura de proyecto

```
src/
├── app.module.ts
├── main.ts
├── common/
│   ├── decorators/
│   ├── filters/
│   ├── guards/
│   ├── interceptors/
│   └── pipes/
├── config/
│   └── configuration.ts    # ConfigModule con validación Joi/Zod
└── modules/
    └── users/
        ├── users.module.ts
        ├── users.controller.ts
        ├── users.service.ts
        ├── dto/
        │   ├── create-user.dto.ts
        │   └── update-user.dto.ts
        └── entities/
            └── user.entity.ts
```

---

## Regla idiomática crítica: SIEMPRE constructor injection

```typescript
// NUNCA — field injection
@Injectable()
export class UsersService {
  @InjectRepository(User)
  private usersRepository: Repository<User>;  // MAL: dificulta testing y viola DI explícita
}

// SIEMPRE — constructor injection
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepository: Repository<User>,
  ) {}
}
```

---

## DTO con class-validator y class-transformer

```typescript
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
import { Transform } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';

export class CreateUserDto {
  @ApiProperty({ example: 'usuario@ejemplo.com' })
  @IsEmail({}, { message: 'El correo no es válido' })
  @Transform(({ value }: { value: unknown }) => {
    if (typeof value === 'string') return value.toLowerCase().trim();
    return value;
  })
  email!: string;

  @ApiProperty({ example: 'contraseñaSegura123', minLength: 8 })
  @IsString()
  @MinLength(8, { message: 'La contraseña debe tener mínimo 8 caracteres' })
  password!: string;

  @ApiProperty({ required: false })
  @IsOptional()
  @IsString()
  nombre?: string;
}
```

---

## Controller con Swagger y decoradores correctos

```typescript
import {
  Controller, Get, Post, Body, Param, HttpCode, HttpStatus, UseGuards,
} from '@nestjs/common';
import {
  ApiTags, ApiBearerAuth, ApiCreatedResponse, ApiOkResponse,
  ApiNotFoundResponse, ApiBadRequestResponse,
} from '@nestjs/swagger';
import { ParseUUIDPipe } from '@nestjs/common';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UserResponseDto } from './dto/user-response.dto';

@ApiTags('usuarios')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('usuarios')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  @HttpCode(HttpStatus.CREATED)
  @ApiCreatedResponse({ type: UserResponseDto, description: 'Usuario creado' })
  @ApiBadRequestResponse({ description: 'Datos de entrada inválidos' })
  async crear(@Body() dto: CreateUserDto): Promise<UserResponseDto> {
    return this.usersService.crear(dto);
  }

  @Get(':id')
  @ApiOkResponse({ type: UserResponseDto })
  @ApiNotFoundResponse({ description: 'Usuario no encontrado' })
  async obtener(
    @Param('id', ParseUUIDPipe) id: string,
  ): Promise<UserResponseDto> {
    return this.usersService.obtenerPorId(id);
  }
}
```

---

## Service con manejo de errores tipado

```typescript
import {
  Injectable, NotFoundException, ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepository: Repository<User>,
  ) {}

  async crear(dto: CreateUserDto): Promise<User> {
    const existe = await this.usersRepository.findOneBy({ email: dto.email });
    if (existe) {
      throw new ConflictException('El correo ya está registrado');
    }
    const hashedPassword = await bcrypt.hash(dto.password, 12);
    const usuario = this.usersRepository.create({
      ...dto,
      password: hashedPassword,
    });
    return this.usersRepository.save(usuario);
  }

  async obtenerPorId(id: string): Promise<User> {
    const usuario = await this.usersRepository.findOneBy({ id });
    if (!usuario) {
      throw new NotFoundException(`Usuario ${id} no encontrado`);
    }
    return usuario;
  }
}
```

---

## Module con exports correctos

```typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { User } from './entities/user.entity';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { JwtStrategy } from '../common/guards/jwt.strategy';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    JwtModule.registerAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        secret: config.getOrThrow<string>('JWT_SECRET'),
        signOptions: { expiresIn: '7d' },
      }),
    }),
  ],
  controllers: [UsersController],
  providers: [UsersService, JwtStrategy],
  exports: [UsersService],  // Exportar solo lo que otros módulos necesitan
})
export class UsersModule {}
```

---

## GlobalPipe y configuración de main.ts

```typescript
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';

async function bootstrap(): Promise<void> {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,               // Elimina propiedades no declaradas en DTO
      forbidNonWhitelisted: true,    // Lanza error si llegan propiedades extra
      transform: true,               // Transforma strings a tipos correctos
      transformOptions: { enableImplicitConversion: true },
    }),
  );

  app.useGlobalFilters(new HttpExceptionFilter());
  app.enableCors({
    origin: process.env['CORS_ORIGIN']?.split(',') ?? [],
    credentials: true,
  });

  const config = new DocumentBuilder()
    .setTitle('API')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);

  const port = process.env['PORT'] ?? 3000;
  await app.listen(port);
}

void bootstrap();
```

---

## MUST DO / MUST NOT DO

**MUST DO:**
- Constructor injection siempre (nunca field injection)
- Validar inputs con class-validator en todos los DTOs
- `whitelist: true` y `forbidNonWhitelisted: true` en ValidationPipe global
- Tipos explícitos en todas las funciones — cero `any`
- `getOrThrow` en ConfigService para variables de entorno requeridas
- Lanzar excepciones tipadas de `@nestjs/common`: NotFoundException, ConflictException, UnauthorizedException, ForbiddenException
- Documentar endpoints con @ApiTags, @ApiOperation, @ApiResponse

**MUST NOT DO:**
- Field injection con @Inject sin constructor
- `any` como tipo — usar `unknown` + type guards
- Lógica de negocio en controllers
- Hardcodear secrets — usar ConfigModule con validación al arranque
- Circular dependencies entre módulos — usar forwardRef() solo si es inevitable
- Olvidar `exports` en módulos cuando otro módulo necesita el servicio
- Omitir `readonly` en dependencias inyectadas del constructor

---

## Gotchas / Errores comunes no obvios

**Dependencia circular silenciosa entre módulos**: cuando el módulo A importa al módulo B y viceversa, NestJS no lanza un error claro — la aplicación arranca pero algunas dependencias son `undefined`. Causa: NestJS no puede resolver el orden de inicialización en un ciclo. Fix: extraer la dependencia compartida a un módulo C que ambos importen, o usar `forwardRef(() => ModuloB)` + `@Inject(forwardRef(() => ServicioB))` en el constructor si el ciclo es inevitable.

**`@InjectRepository` fuera del módulo que declara `TypeOrmModule.forFeature`**: el servicio arranca pero falla con `No repository for "Entidad" was found` en runtime. Causa: `@InjectRepository(Entidad)` requiere que `TypeOrmModule.forFeature([Entidad])` esté en el mismo módulo o que el módulo que lo incluye esté en `exports`. Fix: verificar que el módulo que usa el repositorio tiene `TypeOrmModule.forFeature([Entidad])` en sus `imports`, o que lo importa de otro módulo que lo exporta.

**`ConfigService.get()` retorna `undefined` en guards y middlewares**: ocurre cuando el guard se registra globalmente con `APP_GUARD` antes de que `ConfigModule` termine de inicializar. Causa: orden de inicialización en el contenedor DI. Fix: usar `ConfigService.getOrThrow()` para detectarlo temprano, y verificar que `ConfigModule.forRoot({ isGlobal: true })` está en `AppModule` como primer import.

**ValidationPipe con `transform: true` no convierte tipos en `@Param`**: el parámetro `id` llega como `string` aunque el DTO declare `@IsNumber()`. Causa: `@Param('id')` extrae strings del path — `transform: true` convierte con `class-transformer` pero el valor del path ya es string y no se convierte a number automáticamente sin `@Type(() => Number)`. Fix: agregar `@Type(() => Number)` en el DTO, o usar `ParseIntPipe` / `ParseUUIDPipe` directamente en el decorador `@Param`.

## Tabla de referencias

| Tema | Archivo |
|------|---------|
| Módulos avanzados, DI circular, forwardRef, scopes, dynamic modules | recursos/modulos-di.md |
| Guards JWT, interceptors de logging/caché, exception filters, throttling | recursos/guards-interceptors.md |
| Testing con Jest, Test.createTestingModule, mocks de repositorios, e2e | recursos/testing-nestjs.md |
