Ir al contenido

NestJS

Framework progresivo de Node.js orientado a TypeScript para construir aplicaciones escalables del lado del servidor.

ComandoDescripción
npm i -g @nestjs/cliInstalar NestJS CLI globalmente
nest new project-nameCrear un nuevo proyecto NestJS
nest new project-name --strictCrear proyecto con TypeScript estricto
nest new project-name -p pnpmCrear proyecto usando pnpm
nest new project-name -p yarnCrear proyecto usando Yarn
ComandoDescripción
npm run start:devIniciar servidor de desarrollo con recarga automática
npm run start:debugIniciar con modo debug y recarga automática
npm run start:prodIniciar servidor de producción
npm run buildCompilar el proyecto
npm run testEjecutar tests unitarios
npm run test:watchEjecutar tests en modo observación
npm run test:covEjecutar tests con reporte de cobertura
npm run test:e2eEjecutar tests de extremo a extremo
npm run lintAnalizar código fuente con linter
src/
├── app.controller.ts       # Controlador raíz
├── app.controller.spec.ts  # Test unitario del controlador
├── app.module.ts           # Módulo raíz
├── app.service.ts          # Servicio raíz
├── main.ts                 # Punto de entrada de la aplicación
├── users/
│   ├── users.module.ts     # Módulo de funcionalidad
│   ├── users.controller.ts # Controlador de funcionalidad
│   ├── users.service.ts    # Servicio de funcionalidad
│   ├── dto/
│   │   ├── create-user.dto.ts
│   │   └── update-user.dto.ts
│   ├── entities/
│   │   └── user.entity.ts
│   └── users.controller.spec.ts
├── auth/
│   ├── auth.module.ts
│   ├── auth.guard.ts
│   ├── auth.service.ts
│   └── strategies/
│       └── jwt.strategy.ts
└── common/
    ├── filters/
    │   └── http-exception.filter.ts
    ├── interceptors/
    │   └── logging.interceptor.ts
    └── pipes/
        └── validation.pipe.ts
ComandoDescripción
nest generate module usersGenerar un nuevo módulo
nest generate controller usersGenerar un nuevo controlador
nest generate service usersGenerar un nuevo servicio
nest generate resource usersGenerar recurso CRUD completo (módulo + controlador + servicio + DTOs)
nest generate guard authGenerar un guard de autenticación
nest generate pipe validationGenerar un pipe de validación
nest generate interceptor loggingGenerar un interceptor
nest generate middleware loggerGenerar middleware
nest generate filter http-exceptionGenerar filtro de excepciones
nest generate decorator rolesGenerar un decorador personalizado
nest generate class user.entityGenerar una clase simple
nest generate interface userGenerar una interfaz
nest g res users --no-specGenerar recurso sin archivos de test
nest g mo users --flatGenerar sin crear subcarpeta
nest infoMostrar información del proyecto y dependencias
# Recurso CRUD completo (flujo de trabajo más común)
nest g resource users

# El CLI solicita:
# ? What transport layer do you use? REST API
# ? Would you like to generate CRUD entry points? Yes

# Crea:
# src/users/users.module.ts
# src/users/users.controller.ts
# src/users/users.service.ts
# src/users/dto/create-user.dto.ts
# src/users/dto/update-user.dto.ts
# src/users/entities/user.entity.ts
# src/users/users.controller.spec.ts
# Actualiza: src/app.module.ts (importa UsersModule)
ComandoDescripción
@Module({ imports: [UsersModule] })Importar otro módulo
@Module({ controllers: [UsersController] })Registrar controladores
@Module({ providers: [UsersService] })Registrar proveedores/servicios
@Module({ exports: [UsersService] })Exportar proveedores para otros módulos
@Global()Hacer módulo disponible globalmente (usar con moderación)
import { Module, DynamicModule } from '@nestjs/common'

@Module({})
export class DatabaseModule {
  static forRoot(options: DatabaseOptions): DynamicModule {
    return {
      module: DatabaseModule,
      global: true,
      providers: [
        { provide: 'DATABASE_OPTIONS', useValue: options },
        DatabaseService,
      ],
      exports: [DatabaseService],
    }
  }

  static forRootAsync(options: DatabaseAsyncOptions): DynamicModule {
    return {
      module: DatabaseModule,
      global: true,
      imports: options.imports || [],
      providers: [
        {
          provide: 'DATABASE_OPTIONS',
          useFactory: options.useFactory,
          inject: options.inject || [],
        },
        DatabaseService,
      ],
      exports: [DatabaseService],
    }
  }
}

// Uso en AppModule
@Module({
  imports: [
    DatabaseModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        host: config.get('DB_HOST'),
        port: config.get('DB_PORT'),
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}
ComandoDescripción
@Controller('users')Definir controlador con prefijo de ruta
@Get()Manejar petición GET
@Post()Manejar petición POST
@Put(':id')Manejar petición PUT con parámetro
@Patch(':id')Manejar petición PATCH
@Delete(':id')Manejar petición DELETE
@All('*')Manejar todos los métodos HTTP
ComandoDescripción
@Param('id') id: stringExtraer parámetro de ruta
@Param('id', ParseIntPipe) id: numberExtraer y parsear a entero
@Query('page') page: stringExtraer parámetro de consulta
@Query() query: PaginationDtoExtraer todos los parámetros de consulta como DTO
@Body() dto: CreateUserDtoExtraer y tipar el cuerpo de la petición
@Body('email') email: stringExtraer campo específico del cuerpo
@Headers('authorization') auth: stringExtraer encabezado de la petición
@Ip() ip: stringExtraer dirección IP del cliente
@Req() request: RequestAcceder al objeto request sin procesar
@Res() response: ResponseAcceder al objeto response sin procesar
@HttpCode(201)Establecer código de estado HTTP personalizado
@Header('Cache-Control', 'none')Establecer encabezado de respuesta
@Redirect('/new-url', 301)Redireccionar respuesta
import {
  Controller, Get, Post, Put, Delete, Param, Query, Body,
  HttpCode, HttpStatus, ParseIntPipe, UseGuards, UseInterceptors,
} from '@nestjs/common'
import { UsersService } from './users.service'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { JwtAuthGuard } from '../auth/jwt-auth.guard'
import { LoggingInterceptor } from '../common/interceptors/logging.interceptor'

@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto)
  }

  @Get()
  findAll(
    @Query('page', new ParseIntPipe({ optional: true })) page = 1,
    @Query('limit', new ParseIntPipe({ optional: true })) limit = 10,
  ) {
    return this.usersService.findAll(page, limit)
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.findOne(id)
  }

  @Put(':id')
  @UseGuards(JwtAuthGuard)
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUserDto,
  ) {
    return this.usersService.update(id, updateUserDto)
  }

  @Delete(':id')
  @UseGuards(JwtAuthGuard)
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id)
  }
}
ComandoDescripción
@Injectable()Marcar clase como proveedor inyectable
constructor(private usersService: UsersService)Inyectar servicio vía constructor
@Inject('TOKEN') private configInyectar por token personalizado
{ provide: 'TOKEN', useValue: value }Registrar proveedor de valor
{ provide: 'TOKEN', useFactory: () => ... }Registrar proveedor de fábrica
{ provide: 'TOKEN', useClass: MyClass }Registrar proveedor de clase
{ provide: 'TOKEN', useExisting: OtherService }Alias de proveedor existente
@Optional() private service?: MyServiceInyección de dependencia opcional
{ provide: 'TOKEN', scope: Scope.REQUEST }Proveedor con alcance por petición
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './entities/user.entity'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'

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

  async create(dto: CreateUserDto): Promise<User> {
    const exists = await this.usersRepo.findOneBy({ email: dto.email })
    if (exists) {
      throw new ConflictException('Email already registered')
    }
    const user = this.usersRepo.create(dto)
    return this.usersRepo.save(user)
  }

  async findAll(page: number, limit: number) {
    const [items, total] = await this.usersRepo.findAndCount({
      skip: (page - 1) * limit,
      take: limit,
      order: { createdAt: 'DESC' },
    })
    return { items, total, page, limit }
  }

  async findOne(id: number): Promise<User> {
    const user = await this.usersRepo.findOneBy({ id })
    if (!user) throw new NotFoundException(`User #${id} not found`)
    return user
  }

  async update(id: number, dto: UpdateUserDto): Promise<User> {
    const user = await this.findOne(id)
    Object.assign(user, dto)
    return this.usersRepo.save(user)
  }

  async remove(id: number): Promise<void> {
    const result = await this.usersRepo.delete(id)
    if (result.affected === 0) {
      throw new NotFoundException(`User #${id} not found`)
    }
  }
}
npm install class-validator class-transformer
// main.ts - habilitar validación global
import { ValidationPipe } from '@nestjs/common'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,           // eliminar propiedades desconocidas
    forbidNonWhitelisted: true, // lanzar error en propiedades desconocidas
    transform: true,           // auto-transformar payloads a tipos DTO
    transformOptions: {
      enableImplicitConversion: true, // convertir query strings a números/booleanos
    },
  }))
  await app.listen(3000)
}
import {
  IsString, IsEmail, IsOptional, IsInt, Min, Max,
  MinLength, MaxLength, IsEnum, ValidateNested, IsArray,
} from 'class-validator'
import { Type } from 'class-transformer'
import { PartialType, OmitType, PickType, IntersectionType } from '@nestjs/mapped-types'

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  name: string

  @IsEmail()
  email: string

  @IsString()
  @MinLength(8)
  password: string

  @IsOptional()
  @IsEnum(Role)
  role?: Role

  @IsOptional()
  @ValidateNested()
  @Type(() => AddressDto)
  address?: AddressDto
}

// Auto-generar DTO de actualización (todos los campos opcionales)
export class UpdateUserDto extends PartialType(CreateUserDto) {}

// Seleccionar campos específicos
export class LoginDto extends PickType(CreateUserDto, ['email', 'password']) {}

// Omitir campos específicos
export class PublicUserDto extends OmitType(CreateUserDto, ['password']) {}
ComandoDescripción
@UseGuards(AuthGuard)Aplicar guard a controlador o ruta
@UseGuards(AuthGuard, RolesGuard)Encadenar múltiples guards
app.useGlobalGuards(new AuthGuard())Registrar guard global
canActivate(context: ExecutionContext)Implementar lógica del guard
@SetMetadata('roles', ['admin'])Establecer metadatos personalizados para guards
ReflectorLeer metadatos en guards
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwt
// auth/auth.module.ts
@Module({
  imports: [
    UsersModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        secret: config.get('JWT_SECRET'),
        signOptions: { expiresIn: '1h' },
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [AuthService, JwtStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

// auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(config: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: config.get('JWT_SECRET'),
    })
  }

  validate(payload: { sub: number; email: string }) {
    return { id: payload.sub, email: payload.email }
  }
}

// auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

// Uso
@Controller('profile')
@UseGuards(JwtAuthGuard)
export class ProfileController {
  @Get()
  getProfile(@Req() req) {
    return req.user // inyectado por JwtStrategy.validate()
  }
}
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common'
export const Roles = (...roles: string[]) => SetMetadata('roles', roles)

// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ])
    if (!requiredRoles) return true
    const { user } = context.switchToHttp().getRequest()
    return requiredRoles.some(role => user.roles?.includes(role))
  }
}

// Uso
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
  @Get('dashboard')
  @Roles('admin')
  getDashboard() {
    return { message: 'Admin dashboard' }
  }

  @Delete('users/:id')
  @Roles('admin', 'superadmin')
  removeUser(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id)
  }
}
import { Injectable, NestMiddleware } from '@nestjs/common'
import { Request, Response, NextFunction } from 'express'

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const start = Date.now()
    res.on('finish', () => {
      const duration = Date.now() - start
      console.log(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`)
    })
    next()
  }
}

// Registrar en módulo
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*')
  }
}
import {
  Injectable, NestInterceptor, ExecutionContext, CallHandler,
} from '@nestjs/common'
import { Observable, map, tap } from 'rxjs'

// Transformar forma de la respuesta
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, { data: T }> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<{ data: T }> {
    return next.handle().pipe(
      map(data => ({ data, timestamp: new Date().toISOString() })),
    )
  }
}

// Medir tiempo de ejecución
@Injectable()
export class TimingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const start = Date.now()
    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - start
        const response = context.switchToHttp().getResponse()
        response.setHeader('X-Response-Time', `${duration}ms`)
      }),
    )
  }
}
ComandoDescripción
npm install @nestjs/typeorm typeorm pgInstalar TypeORM con PostgreSQL
TypeOrmModule.forRoot({ type: 'postgres', ... })Configurar conexión en AppModule
TypeOrmModule.forFeature([User])Registrar entidad en módulo de funcionalidad
@InjectRepository(User) private repo: Repository<User>Inyectar repositorio
npm install @prisma/client
npm install -D prisma
npx prisma init
// prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect()
  }

  async onModuleDestroy() {
    await this.$disconnect()
  }
}

// prisma/prisma.module.ts
@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

// Uso en un servicio
@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  findAll() {
    return this.prisma.user.findMany()
  }

  findOne(id: number) {
    return this.prisma.user.findUniqueOrThrow({ where: { id } })
  }
}
ComandoDescripción
throw new BadRequestException('Invalid input')400 Bad Request
throw new UnauthorizedException('Login required')401 Unauthorized
throw new ForbiddenException('Access denied')403 Forbidden
throw new NotFoundException('User not found')404 Not Found
throw new ConflictException('Email taken')409 Conflict
throw new UnprocessableEntityException('Validation failed')422 Unprocessable
throw new InternalServerErrorException()500 Internal Server Error
import {
  ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus,
} from '@nestjs/common'
import { Request, Response } from 'express'

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()

    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR

    const message = exception instanceof HttpException
      ? exception.getResponse()
      : 'Internal server error'

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: typeof message === 'string' ? message : (message as any).message,
    })
  }
}

// Registrar globalmente en main.ts
app.useGlobalFilters(new AllExceptionsFilter())
npm install @nestjs/config
// app.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: ['.env.local', '.env'],
      validationSchema: Joi.object({
        NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
        PORT: Joi.number().default(3000),
        DATABASE_URL: Joi.string().required(),
        JWT_SECRET: Joi.string().required(),
      }),
    }),
  ],
})
export class AppModule {}

// Uso en cualquier servicio
@Injectable()
export class AppService {
  constructor(private config: ConfigService) {}

  getPort(): number {
    return this.config.get<number>('PORT', 3000)
  }
}
import { Test, TestingModule } from '@nestjs/testing'
import { UsersService } from './users.service'
import { getRepositoryToken } from '@nestjs/typeorm'
import { User } from './entities/user.entity'

describe('UsersService', () => {
  let service: UsersService

  const mockRepository = {
    create: jest.fn(),
    save: jest.fn(),
    findOneBy: jest.fn(),
    findAndCount: jest.fn(),
    delete: jest.fn(),
  }

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: getRepositoryToken(User), useValue: mockRepository },
      ],
    }).compile()

    service = module.get<UsersService>(UsersService)
  })

  it('should create a user', async () => {
    const dto = { name: 'Alice', email: 'alice@example.com', password: 'secret123' }
    mockRepository.findOneBy.mockResolvedValue(null)
    mockRepository.create.mockReturnValue(dto)
    mockRepository.save.mockResolvedValue({ id: 1, ...dto })

    const result = await service.create(dto)
    expect(result.id).toBe(1)
    expect(mockRepository.save).toHaveBeenCalled()
  })
})
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication, ValidationPipe } from '@nestjs/common'
import * as request from 'supertest'
import { AppModule } from '../src/app.module'

describe('UsersController (e2e)', () => {
  let app: INestApplication

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile()

    app = moduleFixture.createNestApplication()
    app.useGlobalPipes(new ValidationPipe({ whitelist: true }))
    await app.init()
  })

  afterAll(async () => {
    await app.close()
  })

  it('/users (POST) creates a user', () => {
    return request(app.getHttpServer())
      .post('/users')
      .send({ name: 'Alice', email: 'alice@example.com', password: 'secret123' })
      .expect(201)
      .expect((res) => {
        expect(res.body.name).toBe('Alice')
        expect(res.body.id).toBeDefined()
      })
  })

  it('/users (GET) returns paginated list', () => {
    return request(app.getHttpServer())
      .get('/users?page=1&limit=10')
      .expect(200)
      .expect((res) => {
        expect(res.body.items).toBeInstanceOf(Array)
        expect(res.body.total).toBeDefined()
      })
  })
})
  1. Usa el generador de recursosnest g resource crea todo el scaffold CRUD (módulo, controlador, servicio, DTOs, entidad, test) en segundos y mantiene tu proyecto consistente.

  2. Habilita ValidationPipe global con whitelist — establece whitelist: true y forbidNonWhitelisted: true para eliminar o rechazar automáticamente campos inesperados de todas las peticiones.

  3. Usa tipos mapeados para DTOsPartialType, PickType, OmitType e IntersectionType mantienen tus DTOs DRY derivando DTOs de actualización/consulta del DTO de creación.

  4. Prefiere inyección por constructor — deja que NestJS maneje la inyección de dependencias a través de constructores en lugar de usar tokens @Inject() manuales a menos que necesites proveedores personalizados.

  5. Mantén los controladores delgados — los controladores solo deben parsear la entrada y devolver la salida. Mueve toda la lógica de negocio a los servicios para testabilidad y reutilización.

  6. Usa ConfigModule para todo acceso al entorno — nunca leas process.env directamente en los servicios. Usa ConfigService con esquemas de validación para que las variables faltantes fallen rápido al inicio.

  7. Aplica guards e interceptores apropiadamente — aplícalos a nivel de controlador o ruta con decoradores en lugar de globalmente, a menos que realmente apliquen en todas partes.

  8. Escribe tests unitarios y e2e — testea unitariamente los servicios con dependencias mockeadas usando Test.createTestingModule(), y testea los endpoints HTTP de extremo a extremo con supertest.

  9. Usa Prisma o TypeORM consistentemente — elige un ORM y úsalo en todo el proyecto. Prisma ofrece mejor seguridad de tipos; TypeORM ofrece más flexibilidad con decoradores.

  10. Estructura por funcionalidad, no por tipo — agrupa archivos por dominio (users/, auth/, orders/) en lugar de por tipo (controllers/, services/). Esto mantiene el código relacionado junto y los módulos autocontenidos.

  11. Maneja errores con excepciones — lanza excepciones HTTP integradas (NotFoundException, ConflictException) desde los servicios. Usa filtros de excepciones para formateo personalizado de errores.

  12. Usa async/await consistentemente — NestJS maneja promesas nativamente. Retorna valores async desde los métodos de servicio y deja que el framework serialice la respuesta.