NestJS con TypeScript: decoradores, DI modular, Pipes de validación y Guards de autenticación

NestJS es el framework de Node.js que más integra TypeScript en su diseño: los decoradores son la API principal, la inyección de dependencias es completamente tipada y cada módulo declara explícitamente sus proveedores y exportaciones. Con las herramientas adecuadas, una API REST en NestJS con TypeScript tiene validación automática, autenticación mediante Guards y logging transparente mediante Interceptors.

Módulo, controlador y servicio básicos

// src/usuarios/usuarios.module.ts
import { Module } from '@nestjs/common';
import { UsuariosController } from './usuarios.controller';
import { UsuariosService } from './usuarios.service';

@Module({
  controllers: [UsuariosController],
  providers: [UsuariosService],
  exports: [UsuariosService],   // otros módulos pueden inyectarlo
})
export class UsuariosModule {}

// src/usuarios/usuarios.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class UsuariosService {
  private usuarios: Usuario[] = [];

  obtenerTodos(): Usuario[] { return this.usuarios; }
  obtenerPorId(id: number): Usuario | undefined {
    return this.usuarios.find((u) => u.id === id);
  }
}

// src/usuarios/usuarios.controller.ts
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import { UsuariosService } from './usuarios.service';

@Controller('usuarios')
export class UsuariosController {
  constructor(private readonly service: UsuariosService) {}

  @Get()
  obtenerTodos(): Usuario[] {
    return this.service.obtenerTodos();
  }

  @Get(':id')
  obtenerPorId(@Param('id', ParseIntPipe) id: number): Usuario {
    // ParseIntPipe convierte el string del parámetro a number con tipo correcto
    const usuario = this.service.obtenerPorId(id);
    if (!usuario) throw new NotFoundException();
    return usuario;
  }
}

Validación automática con ValidationPipe y class-validator

// src/usuarios/dto/crear-usuario.dto.ts
import { IsEmail, IsString, MinLength, IsOptional, IsInt, Min } from 'class-validator';

export class CrearUsuarioDto {
  @IsString()
  @MinLength(2)
  nombre!: string;

  @IsEmail()
  email!: string;

  @IsString()
  @MinLength(8)
  contrasena!: string;

  @IsOptional()
  @IsInt()
  @Min(0)
  edad?: number;
}

// src/main.ts — activar ValidationPipe globalmente:
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';

const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,      // elimina campos no declarados en el DTO
  forbidNonWhitelisted: true, // error si llegan campos extra
  transform: true,      // convierte los tipos automáticamente
}));

Guard de JWT tipado

// src/auth/jwt.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import type { Request } from 'express';

interface JwtPayload {
  sub: number;
  email: string;
  rol: 'admin' | 'usuario';
}

// Extender Request para añadir el usuario autenticado:
declare module 'express' {
  interface Request {
    usuario?: JwtPayload;
  }
}

@Injectable()
export class JwtGuard implements CanActivate {
  constructor(private jwt: JwtService) {}

  async canActivate(ctx: ExecutionContext): Promise<boolean> {
    const req = ctx.switchToHttp().getRequest<Request>();
    const token = req.headers.authorization?.replace('Bearer ', '');
    if (!token) throw new UnauthorizedException();

    req.usuario = await this.jwt.verifyAsync<JwtPayload>(token);
    return true;
  }
}

Decorador @CurrentUser

// src/auth/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import type { Request } from 'express';
import type { JwtPayload } from './jwt.guard';

export const CurrentUser = createParamDecorator(
  (_: unknown, ctx: ExecutionContext): JwtPayload => {
    const req = ctx.switchToHttp().getRequest<Request>();
    return req.usuario!;
  }
);

// Uso en el controlador:
@Get('perfil')
@UseGuards(JwtGuard)
obtenerPerfil(@CurrentUser() usuario: JwtPayload) {
  // usuario es JwtPayload, completamente tipado
  return { id: usuario.sub, email: usuario.email };
}

Interceptor de logging

// src/common/logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
import type { Request } from 'express';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(LoggingInterceptor.name);

  intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
    const req = ctx.switchToHttp().getRequest<Request>();
    const inicio = Date.now();

    return next.handle().pipe(
      tap(() => {
        const duracion = Date.now() - inicio;
        this.logger.log(`${req.method} ${req.path} — ${duracion}ms`);
      })
    );
  }
}

Imagen: Pexels / Pixabay

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP