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
