Introducción
NestJS es hoy el framework de backend Node.js más usado para construir aplicaciones server-side escalables, tipadas y mantenibles. Combina los conceptos más sólidos de la ingeniería de software — Dependency Injection, módulos, arquitectura en capas — con la productividad de TypeScript y un ecosistema que cubre prácticamente cualquier necesidad: REST, GraphQL, WebSocket, microservicios, CLI, cron jobs.
En esta guía construimos, paso a paso, una REST API completa y lista para producción: una API de Blog con autenticación JWT, refresh token, control de acceso basado en roles (RBAC), validación, subida de archivos, logging centralizado y gestión estructurada de errores — exactamente el stack detrás de la mayoría de las APIs NestJS reales en 2026.
Qué es NestJS
NestJS es un framework opinionado construido sobre Express (o, alternativamente, Fastify) que impone una estructura precisa a la aplicación: cada funcionalidad se organiza en módulos, cada módulo expone controllers (gestionan las peticiones HTTP) y providers (contienen la lógica de negocio, típicamente services), conectados entre sí mediante un sistema de Dependency Injection integrado e inspirado en Angular.
Por Qué Usarlo
- TypeScript nativo: tipado end-to-end, autocompletado, refactoring seguro.
- Arquitectura impuesta: a diferencia de Express puro, NestJS obliga a separar responsabilidades (controller/service/repository), reduciendo el "big ball of mud" típico de proyectos Node que crecieron sin estructura.
- Dependency Injection integrada: testabilidad elevada, bajo acoplamiento, providers intercambiables (útil para mocks en los tests).
- Ecosistema maduro: módulos oficiales para TypeORM, Prisma, Mongoose, Passport, GraphQL, WebSocket, Bull/BullMQ, Swagger, gRPC, microservicios.
- Decoradores declarativos: guards, interceptors, pipes y exception filters permiten cross-cutting concerns (auth, logging, validación) sin contaminar la lógica de negocio.
Cuándo Conviene (y Cuándo No)
NestJS conviene cuando el proyecto tiene una complejidad que justifica una estructura rígida: equipos con varios desarrolladores, APIs destinadas a crecer con el tiempo, necesidad de testabilidad elevada, aplicaciones enterprise con requisitos de seguridad y compliance. Es probablemente excesivo para un script puntual o un prototipo que se descartará en una semana — ahí, Express puro o Fastify "desnudo" siguen siendo más rápidos de arrancar.
Ventajas y Desventajas
| Ventajas | Desventajas |
|---|---|
| Estructura clara y consistente entre proyectos distintos | Curva de aprendizaje más pronunciada que Express puro (decoradores, DI, módulos) |
| Testabilidad nativa gracias al DI | Overhead de boilerplate en proyectos muy pequeños |
| Ecosistema oficial amplio y bien mantenido | Más "magia" (decoradores, reflection) frente a código explícito |
| Excelente integración con TypeScript y Swagger/OpenAPI | Bundle size y cold start ligeramente superiores a frameworks minimalistas (relevante en entornos serverless) |
| Facilita adoptar Clean Architecture / hexagonal | Requiere disciplina del equipo para no abusar de la flexibilidad de los módulos |
Casos Reales
NestJS se usa en producción por empresas como Adidas, Roche, Autodesk y Decathlon, además de ser una elección muy común para backends SaaS B2B, plataformas de e-commerce, sistemas de gestión de usuarios y APIs de microservicios que necesitan comunicarse entre sí vía gRPC o colas de mensajes (RabbitMQ, Kafka).
Requisitos Previos
Antes de empezar, asegúrate de tener estas herramientas instaladas y de conocer los conceptos básicos de TypeScript (interfaces, decoradores, generics) y de REST (verbos HTTP, códigos de estado, idempotencia).
| Herramienta | Versión recomendada | Para qué sirve |
|---|---|---|
| Node.js | 20.x LTS o superior | Runtime JavaScript sobre el que corre NestJS |
| npm | 10.x (incluido en Node 20) | Gestión de paquetes |
| NestJS CLI | @nestjs/cli 10.x+ | Scaffolding de módulos, controllers, services |
| TypeScript | 5.x | Lenguaje en el que están escritos NestJS y tu app |
| VS Code | Última versión estable | Editor con soporte nativo para TypeScript |
| PostgreSQL | 15.x o superior | Base de datos relacional para el ejemplo con TypeORM |
| Docker (opcional) | 24.x+ | Ejecutar PostgreSQL localmente sin instalación nativa |
# Verifica las versiones instaladas
node -v
npm -v
# Instala la CLI de NestJS globalmente
npm install -g @nestjs/cli
nest --version
Consejo: si no quieres instalar PostgreSQL de forma nativa, levántalo con Docker:
docker run --name pg-blog -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=blog_db -p 5432:5432 -d postgres:16.
Arquitectura
Antes de escribir código, es fundamental entender cómo NestJS enruta una petición HTTP a través de sus componentes principales. Este es el ciclo de vida completo de una petición:
Client
│
▼
┌─────────────┐
│ Middleware │ (ej. logger, cookie-parser — se ejecuta antes del routing)
└──────┬───────┘
▼
┌─────────────┐
│ Guard │ (ej. AuthGuard — decide si la petición puede continuar)
└──────┬───────┘
▼
┌─────────────┐
│ Interceptor │ (fase "antes" — ej. logging, transformación del input)
└──────┬───────┘
▼
┌─────────────┐
│ Pipe │ (valida y transforma los parámetros de entrada)
└──────┬───────┘
▼
┌─────────────┐
│ Controller │ (recibe la petición, delega en el service)
└──────┬───────┘
▼
┌─────────────┐
│ Service │ (lógica de negocio, llama a los repositories)
└──────┬───────┘
▼
┌─────────────┐
│ Repository / │ (acceso a datos — TypeORM, Prisma, Mongoose...)
│ Database │
└──────┬───────┘
▼
┌─────────────┐
│ Interceptor │ (fase "después" — ej. transformación de la respuesta)
└──────┬───────┘
▼
┌──────────────────┐
│ Exception Filter │ (intercepta SOLO si se lanza una excepción)
└──────┬────────────┘
▼
Response
Controller
La capa más externa: recibe las peticiones HTTP, extrae parámetros/body/query y delega inmediatamente la lógica al service correspondiente. Un controller nunca debe contener lógica de negocio — su única responsabilidad es el mapeo HTTP ↔ llamada a método.
Service
Contiene la lógica de negocio propiamente dicha. Es un provider inyectable,
típicamente marcado con @Injectable(), y se inyecta en el controller (o en otros
services) a través del constructor.
Module
El módulo es la unidad organizativa de NestJS: agrupa controllers, providers e imports de
otros módulos. Toda aplicación tiene al menos un AppModule raíz, y las
funcionalidades se aíslan típicamente en feature modules (ej.
PostsModule, AuthModule, UsersModule).
Provider
Cualquier clase gestionada por el contenedor de Dependency Injection de Nest: services,
repositories, factories, helpers. Se declara en providers dentro del módulo y
puede inyectarse donde esté disponible (en el mismo módulo, o exportado a otros módulos).
Middleware
Funciones ejecutadas antes del routing de Nest, con acceso directo a
req, res y next() — el mismo modelo que Express. Útil
para logging en bruto, parsing de cookies, o headers personalizados aplicados globalmente.
Guard
Deciden si una petición puede continuar, devolviendo true/
false (o lanzando una excepción). Son el lugar correcto para autenticación y
autorización — nunca dentro del controller o del service.
Interceptor
Se posicionan alrededor de la ejecución del handler de la ruta (como middleware AOP): pueden transformar la petición antes de que llegue al controller y la respuesta antes de que salga. Casos de uso típicos: logging de tiempos de respuesta, transformación uniforme de las respuestas, caching, gestión de timeouts.
Pipe
Transforman y validan los datos de entrada (parámetros de ruta, query string, body) antes de
que lleguen al controller. ValidationPipe, integrado con
class-validator, es el pipe más usado con diferencia en cualquier API NestJS
seria.
Exception Filter
Interceptan las excepciones lanzadas en cualquier punto de la petición y las transforman en una respuesta HTTP consistente (status code, cuerpo JSON estructurado), evitando que stack traces o errores en bruto lleguen al cliente.
Instalación
# 1. Crea un nuevo proyecto NestJS
nest new blog-api
# Durante la creación, elige npm como package manager cuando se solicite
cd blog-api
# 2. Instala las dependencias para la base de datos (TypeORM + driver PostgreSQL)
npm install @nestjs/typeorm typeorm pg
# 3. Instala las dependencias para la validación
npm install class-validator class-transformer
# 4. Instala las dependencias para la autenticación JWT
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm install --save-dev @types/passport-jwt @types/bcrypt
# 5. Instala las dependencias para seguridad y rate limiting
npm install helmet @nestjs/throttler
# 6. Instala las dependencias para la subida de archivos
npm install @nestjs/platform-express multer
npm install --save-dev @types/multer
# 7. Instala Swagger para la documentación automática de la API
npm install @nestjs/swagger
# 8. Instala el logger estructurado
npm install nestjs-pino pino-http pino-pretty
# 9. Configura las variables de entorno
npm install @nestjs/config
Cada comando instala un bloque funcional muy preciso: @nestjs/typeorm +
typeorm + pg conectan Nest con PostgreSQL a través del ORM TypeORM;
class-validator/class-transformer habilitan DTOs validados
automáticamente; el stack passport/passport-jwt/bcrypt
construye todo el flujo de autenticación; helmet y
@nestjs/throttler refuerzan la seguridad HTTP y el rate limiting;
multer gestiona el multipart/form-data para las subidas; nestjs-pino
proporciona logging estructurado en JSON, adecuado para producción.
Implementación Paso a Paso
1. Estructura de Carpetas
src/
├── main.ts
├── app.module.ts
├── config/
│ └── configuration.ts
├── common/
│ ├── filters/
│ │ └── http-exception.filter.ts
│ ├── interceptors/
│ │ ├── logging.interceptor.ts
│ │ └── transform.interceptor.ts
│ ├── guards/
│ │ └── roles.guard.ts
│ ├── decorators/
│ │ └── roles.decorator.ts
│ └── pipes/
│ └── parse-object-id.pipe.ts
├── auth/
│ ├── auth.module.ts
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ ├── strategies/
│ │ ├── jwt.strategy.ts
│ │ └── jwt-refresh.strategy.ts
│ └── dto/
│ ├── login.dto.ts
│ └── register.dto.ts
├── users/
│ ├── users.module.ts
│ ├── users.service.ts
│ └── entities/
│ └── user.entity.ts
└── posts/
├── posts.module.ts
├── posts.controller.ts
├── posts.service.ts
├── entities/
│ └── post.entity.ts
└── dto/
├── create-post.dto.ts
└── update-post.dto.ts
2. Configuración Centralizada con @nestjs/config
// src/config/configuration.ts
export default () => ({
port: parseInt(process.env.PORT ?? '3000', 10),
database: {
host: process.env.DB_HOST ?? 'localhost',
port: parseInt(process.env.DB_PORT ?? '5432', 10),
username: process.env.DB_USERNAME ?? 'postgres',
password: process.env.DB_PASSWORD ?? 'postgres',
name: process.env.DB_NAME ?? 'blog_db',
},
jwt: {
accessSecret: process.env.JWT_ACCESS_SECRET ?? 'change-me-access',
accessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN ?? '15m',
refreshSecret: process.env.JWT_REFRESH_SECRET ?? 'change-me-refresh',
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? '7d',
},
});
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ThrottlerModule } from '@nestjs/throttler';
import configuration from './config/configuration';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { PostsModule } from './posts/posts.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: [configuration] }),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
type: 'postgres',
host: cfg.get('database.host'),
port: cfg.get('database.port'),
username: cfg.get('database.username'),
password: cfg.get('database.password'),
database: cfg.get('database.name'),
autoLoadEntities: true,
synchronize: process.env.NODE_ENV !== 'production', // ⚠️ solo en desarrollo
}),
}),
ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]),
AuthModule,
UsersModule,
PostsModule,
],
})
export class AppModule {}
Línea por línea: ConfigModule.forRoot({ isGlobal: true }) hace
que ConfigService esté disponible en toda la aplicación sin tener que
reimportar el módulo en todas partes. TypeOrmModule.forRootAsync construye la
conexión a la base de datos de forma asíncrona, leyendo los valores desde
ConfigService en lugar de un objeto estático — necesario para poder usar
variables de entorno. synchronize: true hace que TypeORM cree/actualice
automáticamente las tablas según las entities: muy cómodo en desarrollo,
peligroso en producción (puede borrar datos), donde se usarán migrations en
su lugar.
3. Entities con TypeORM
// src/users/entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, OneToMany } from 'typeorm';
import { Post } from '../../posts/entities/post.entity';
import { Exclude } from 'class-transformer';
export enum UserRole {
ADMIN = 'admin',
AUTHOR = 'author',
READER = 'reader',
}
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column()
@Exclude() // excluye el hash de la contraseña de las respuestas serializadas
passwordHash: string;
@Column({ type: 'enum', enum: UserRole, default: UserRole.READER })
role: UserRole;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
@CreateDateColumn()
createdAt: Date;
}
// src/posts/entities/post.entity.ts
import {
Entity, Column, PrimaryGeneratedColumn, ManyToOne,
CreateDateColumn, UpdateDateColumn, Index,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@Index({ unique: true })
@Column()
slug: string;
@Column('text')
content: string;
@Column({ default: false })
published: boolean;
@Column({ nullable: true })
coverImageUrl?: string;
@ManyToOne(() => User, (user) => user.posts, { eager: true })
author: User;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
4. DTOs con class-validator
// src/posts/dto/create-post.dto.ts
import { IsString, IsBoolean, IsOptional, MinLength, MaxLength } from 'class-validator';
export class CreatePostDto {
@IsString()
@MinLength(5)
@MaxLength(200)
title: string;
@IsString()
@MinLength(20)
content: string;
@IsBoolean()
@IsOptional()
published?: boolean;
}
// src/posts/dto/update-post.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreatePostDto } from './create-post.dto';
export class UpdatePostDto extends PartialType(CreatePostDto) {}
PartialType genera automáticamente una versión donde todos los campos del DTO
original se vuelven opcionales — perfecto para las actualizaciones parciales
(PATCH), sin duplicar los decoradores de validación.
5. Habilitar ValidationPipe Globalmente
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import helmet from 'helmet';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(helmet());
app.enableCors({ origin: process.env.CORS_ORIGIN?.split(',') ?? '*' });
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // elimina las propiedades no presentes en el DTO
forbidNonWhitelisted: true, // lanza un error si llegan propiedades extra
transform: true, // convierte automáticamente los tipos (ej. string → number)
}),
);
app.useGlobalFilters(new HttpExceptionFilter());
app.setGlobalPrefix('api/v1');
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
whitelist: true + forbidNonWhitelisted: true son el par más
importante para la seguridad de los DTOs: sin ello, un cliente podría enviar campos extra
(ej. role: 'admin' en una petición de registro) que TypeORM podría persistir
inadvertidamente si el código no los filtra explícitamente en otro lugar.
6. Middleware Personalizado
// src/common/middleware/request-id.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
req['requestId'] = randomUUID();
res.setHeader('X-Request-Id', req['requestId']);
next();
}
}
// registro en app.module.ts
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestIdMiddleware).forRoutes('*');
}
}
7. Guards: JwtAuthGuard y RolesGuard
// src/auth/strategies/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.get('jwt.accessSecret'),
});
}
async validate(payload: { sub: string; email: string; role: string }) {
// El valor devuelto se adjunta a req.user
return { userId: payload.sub, email: payload.email, role: payload.role };
}
}
// src/common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { UserRole } from '../../users/entities/user.entity';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);
// src/common/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
import { UserRole } from '../../users/entities/user.entity';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true; // sin restricción de rol en esta ruta
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user?.role);
}
}
El patrón Reflector.getAllAndOverride lee los metadatos establecidos por el
decorador @Roles(...) tanto a nivel de método individual como de clase entera,
permitiendo definir una restricción de rol en todo un controller y sobrescribirla en rutas
individuales cuando sea necesario.
8. Interceptors: Logging y Transformación de la Respuesta
// src/common/interceptors/logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable {
const req = context.switchToHttp().getRequest();
const start = Date.now();
return next.handle().pipe(
tap(() => {
const ms = Date.now() - start;
this.logger.log(`${req.method} ${req.url} — ${ms}ms`);
}),
);
}
}
// src/common/interceptors/transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response {
success: true;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor implements NestInterceptor> {
intercept(context: ExecutionContext, next: CallHandler): Observable> {
return next.handle().pipe(
map((data) => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
9. Exception Filter Global
// src/common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger('ExceptionFilter');
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception instanceof HttpException
? exception.getResponse()
: 'Errore interno del server';
// Registra el stack trace completo solo en el servidor, nunca en la respuesta al cliente
if (!(exception instanceof HttpException)) {
this.logger.error(exception instanceof Error ? exception.stack : exception);
}
response.status(status).json({
success: false,
statusCode: status,
path: request.url,
timestamp: new Date().toISOString(),
message,
});
}
}
Un detalle de seguridad crucial: cuando la excepción no es una
HttpException conocida (es decir, un error inesperado, ej. un bug o un error de
la base de datos), el mensaje devuelto al cliente es genérico ("Error interno del servidor")
— el stack trace real solo se registra en el servidor. Exponer stack traces a los clientes es
un problema de seguridad conocido (information disclosure).
10. Pipe Personalizado
// src/common/pipes/parse-uuid-or-404.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, NotFoundException } from '@nestjs/common';
import { isUUID } from 'class-validator';
@Injectable()
export class ParseUuidOr404Pipe implements PipeTransform {
transform(value: string, _metadata: ArgumentMetadata): string {
if (!isUUID(value)) {
throw new NotFoundException(`Risorsa con id "${value}" non trovata`);
}
return value;
}
}
Devolver un 404 en lugar de un genérico 400 Bad Request cuando el id
ni siquiera es un UUID válido es una elección deliberada: desde el punto de vista del cliente,
"recurso no encontrado" es semánticamente más correcto y no revela detalles sobre el formato
interno de los ids.
Ejemplo Real: API de Blog Completa
Vamos a unir todas las piezas en un módulo PostsModule completo, con CRUD,
autenticación, autorización basada en roles y subida de la imagen de portada.
Auth Service: login, register, refresh token
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
import { UsersService } from '../users/users.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private config: ConfigService,
) {}
async register(dto: RegisterDto) {
const existing = await this.usersService.findByEmail(dto.email);
if (existing) throw new ConflictException('Email già registrata');
const passwordHash = await bcrypt.hash(dto.password, 12);
const user = await this.usersService.create({ ...dto, passwordHash });
return this.buildTokens(user.id, user.email, user.role);
}
async login(dto: LoginDto) {
const user = await this.usersService.findByEmail(dto.email);
if (!user) throw new UnauthorizedException('Credenziali non valide');
const passwordValid = await bcrypt.compare(dto.password, user.passwordHash);
if (!passwordValid) throw new UnauthorizedException('Credenziali non valide');
return this.buildTokens(user.id, user.email, user.role);
}
async refresh(userId: string, email: string, role: string) {
// En producción: verifica también que el refresh token siga siendo válido en el servidor
// (whitelist/blacklist) para poder revocarlo, ej. en el logout.
return this.buildTokens(userId, email, role);
}
private buildTokens(sub: string, email: string, role: string) {
const payload = { sub, email, role };
const accessToken = this.jwtService.sign(payload, {
secret: this.config.get('jwt.accessSecret'),
expiresIn: this.config.get('jwt.accessExpiresIn'),
});
const refreshToken = this.jwtService.sign(payload, {
secret: this.config.get('jwt.refreshSecret'),
expiresIn: this.config.get('jwt.refreshExpiresIn'),
});
return { accessToken, refreshToken };
}
}
¿Por qué dos tokens separados? El access token tiene una vida corta (15 minutos) y es el que se envía en cada petición — si lo roban, el daño está limitado en el tiempo. El refresh token tiene una vida más larga (días) pero solo se usa para obtener un nuevo access token en un endpoint dedicado, reduciendo la superficie de ataque.
Auth Controller
// src/auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
@UseGuards(AuthGuard('jwt-refresh'))
@Post('refresh')
refresh(@Req() req) {
return this.authService.refresh(req.user.userId, req.user.email, req.user.role);
}
}
Posts Controller: CRUD, RBAC y Subida de Archivos
// src/posts/posts.controller.ts
import {
Controller, Get, Post, Body, Patch, Param, Delete,
UseGuards, UseInterceptors, UploadedFile, Query,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { AuthGuard } from '@nestjs/passport';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { Roles } from '../common/decorators/roles.decorator';
import { RolesGuard } from '../common/guards/roles.guard';
import { UserRole } from '../users/entities/user.entity';
import { ParseUuidOr404Pipe } from '../common/pipes/parse-uuid-or-404.pipe';
import { CurrentUser } from '../common/decorators/current-user.decorator';
@Controller('posts')
export class PostsController {
constructor(private postsService: PostsService) {}
@Get()
findAll(@Query('page') page = 1, @Query('limit') limit = 10) {
return this.postsService.findAll(Number(page), Number(limit));
}
@Get(':id')
findOne(@Param('id', ParseUuidOr404Pipe) id: string) {
return this.postsService.findOne(id);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.AUTHOR, UserRole.ADMIN)
@Post()
create(@Body() dto: CreatePostDto, @CurrentUser() user) {
return this.postsService.create(dto, user.userId);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.AUTHOR, UserRole.ADMIN)
@Patch(':id')
update(@Param('id', ParseUuidOr404Pipe) id: string, @Body() dto: UpdatePostDto, @CurrentUser() user) {
return this.postsService.update(id, dto, user.userId, user.role);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.ADMIN)
@Delete(':id')
remove(@Param('id', ParseUuidOr404Pipe) id: string) {
return this.postsService.remove(id);
}
@UseGuards(AuthGuard('jwt'))
@Post(':id/cover')
@UseInterceptors(FileInterceptor('file', {
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
fileFilter: (_req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
cb(null, allowed.includes(file.mimetype));
},
}))
uploadCover(@Param('id', ParseUuidOr404Pipe) id: string, @UploadedFile() file: Express.Multer.File) {
return this.postsService.setCoverImage(id, file);
}
}
Observa cómo cada ruta aplica solo los guards y roles estrictamente
necesarios: findAll/findOne son públicas (sin guard),
create/update requieren el rol author o
admin, mientras que remove está reservada solo a
admin — un ejemplo concreto del principio de mínimo privilegio
aplicado ruta por ruta.
Posts Service
// src/posts/posts.service.ts
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Post } from './entities/post.entity';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { UserRole } from '../users/entities/user.entity';
import slugify from 'slugify';
@Injectable()
export class PostsService {
constructor(@InjectRepository(Post) private postsRepo: Repository) {}
async findAll(page: number, limit: number) {
const [items, total] = await this.postsRepo.findAndCount({
where: { published: true },
order: { createdAt: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});
return { items, total, page, limit };
}
async findOne(id: string): Promise {
const post = await this.postsRepo.findOne({ where: { id } });
if (!post) throw new NotFoundException(`Post ${id} non trovato`);
return post;
}
async create(dto: CreatePostDto, authorId: string): Promise {
const post = this.postsRepo.create({
...dto,
slug: slugify(dto.title, { lower: true, strict: true }),
author: { id: authorId } as any,
});
return this.postsRepo.save(post);
}
async update(id: string, dto: UpdatePostDto, userId: string, userRole: UserRole): Promise {
const post = await this.findOne(id);
// Un author solo puede editar sus propios posts; el admin puede editarlos todos
if (userRole !== UserRole.ADMIN && post.author.id !== userId) {
throw new ForbiddenException('Non puoi modificare un post di un altro autore');
}
Object.assign(post, dto);
return this.postsRepo.save(post);
}
async remove(id: string): Promise {
const result = await this.postsRepo.delete(id);
if (result.affected === 0) throw new NotFoundException(`Post ${id} non trovato`);
}
async setCoverImage(id: string, file: Express.Multer.File): Promise {
const post = await this.findOne(id);
// En producción: sube a S3/Cloud Storage en lugar de disco local
post.coverImageUrl = `/uploads/${file.filename}`;
return this.postsRepo.save(post);
}
}
Observa la comprobación userRole !== UserRole.ADMIN && post.author.id !== userId
en el método update: es un ejemplo de autorización a nivel de
recurso (no solo de ruta) — un author autenticado con un token JWT válido puede aun
así ser bloqueado si está intentando modificar el recurso de otra persona. Esta comprobación
siempre debe hacerse en el service, nunca delegarse solo al guard, que opera a un nivel
demasiado genérico para conocer al propietario de un recurso específico.
Buenas Prácticas
| Principio | Aplicación Práctica en NestJS |
|---|---|
| Clean Architecture | Separación clara entre controller (HTTP), service (lógica de negocio) y repository (persistencia). El dominio nunca debe depender de detalles de infraestructura como Express o TypeORM. |
| SOLID — Single Responsibility | Un controller mapea peticiones HTTP, un service contiene una sola área de lógica de negocio, un repository un solo aggregate. |
| SOLID — Dependency Inversion | Los services dependen de interfaces/tokens abstractos (ej. @InjectRepository), no de implementaciones concretas — sustituibles por mocks en los tests. |
| DRY | Lógica de validación compartida en los DTOs con PartialType/PickType/OmitType, evitando duplicación entre el DTO de Create y el de Update. |
| KISS | Evita introducir CQRS, Event Sourcing o microservicios hasta que la complejidad real del dominio lo justifique. |
| Repository Pattern | TypeORM/Prisma ya proporcionan esta capa; evita "agujerear" la abstracción llamando a queries SQL en bruto directamente en los services salvo en casos críticos de rendimiento. |
| DTO | Cada endpoint tiene un DTO de entrada dedicado — nunca exponer directamente las entities de la base de datos como contrato de API. |
| Validation | ValidationPipe global con whitelist y forbidNonWhitelisted siempre activos en todos los proyectos, sin excepciones. |
Rendimiento
Caching
// Caching a nivel de endpoint con @nestjs/cache-manager
import { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager';
import { UseInterceptors } from '@nestjs/common';
@UseInterceptors(CacheInterceptor)
@CacheTTL(60) // cache durante 60 segundos
@Get()
findAll() {
return this.postsService.findAll(1, 10);
}
Lazy Loading de Módulos
En aplicaciones monolíticas de gran tamaño, LazyModuleLoader permite cargar
módulos poco usados (ej. un módulo de informes) solo cuando realmente se necesitan,
reduciendo el tiempo de bootstrap.
Queries Optimizadas
- Usa
selectexplícito en TypeORM para evitar transferir columnas innecesarias (ej. excluircontenten las listas). - Añade índices a las columnas usadas con frecuencia en
WHERE/ORDER BY(ver el@Indexen el slug de la entityPost). - Evita el problema N+1: usa
relationsoQueryBuilderconleftJoinAndSelecten lugar de cargar relaciones dentro de un bucle. - Usa siempre paginación (
skip/take) — nunca devuelvas colecciones ilimitadas.
Logging y Monitorización
En producción, sustituye el logger por defecto por nestjs-pino para logs JSON
estructurados, fácilmente indexables por herramientas como Grafana Loki o Datadog. Para la
monitorización del rendimiento, integra @nestjs/terminus para los health checks
(/health) usados por load balancers y orquestadores (Kubernetes, Railway).
Profiling
Para encontrar cuellos de botella reales, usa node --prof o herramientas APM
(Application Performance Monitoring) como New Relic o Elastic APM, que rastrean el tiempo
empleado en cada handler, query y llamada externa — evita optimizar "a ojo" sin datos reales.
Seguridad
| Medida | Cómo se Implementa |
|---|---|
| JWT | Access token de vida corta firmado con un secret robusto (32+ caracteres aleatorios), nunca hardcodeado en el código. |
| Refresh Token | Vida más larga, idealmente revocable (whitelist en Redis/DB), rotado en cada uso. |
| Hash de Contraseña / bcrypt | bcrypt.hash(password, 12) — nunca MD5/SHA1, nunca contraseñas en texto plano en la BD. |
| Helmet | app.use(helmet()) establece headers de seguridad (CSP, X-Frame-Options, HSTS) en una sola línea. |
| CORS | app.enableCors({ origin: [...] }) con whitelist explícita de dominios, nunca origin: '*' en producción si la API requiere credentials. |
| Rate Limiting | @nestjs/throttler para limitar peticiones por IP, esencial en endpoints como /auth/login para mitigar ataques de fuerza bruta. |
| ValidationPipe | whitelist + forbidNonWhitelisted para bloquear mass assignment en campos no previstos. |
| Sanitización de Input | class-validator para la estructura, más sanitización explícita de HTML (ej. sanitize-html) si un campo permite markup libre, para prevenir XSS. |
// Ejemplo: rate limiting más agresivo específicamente en el login
import { Throttle } from '@nestjs/throttler';
@Throttle({ default: { limit: 5, ttl: 60000 } }) // máx. 5 intentos por minuto por IP
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
Errores Comunes
1. Olvidar whitelist/forbidNonWhitelisted en ValidationPipe
Problema: un cliente puede enviar campos extra no previstos por el DTO (ej. role: 'admin').
Causa: ValidationPipe configurado sin whitelist: true.
Solución: activa siempre whitelist y forbidNonWhitelisted globalmente en main.ts.
2. synchronize: true en Producción
Problema: TypeORM puede alterar/borrar columnas o tablas en tiempo de ejecución.
Causa: confusión entre el entorno de desarrollo y producción en la configuración de la base de datos.
Solución: synchronize: false en producción, gestiona el esquema con migrations explícitas (typeorm migration:generate).
3. Dependencia Circular Entre Módulos
Problema: error "Nest cannot resolve dependencies" o un crash silencioso al arrancar.
Causa: dos módulos se importan mutuamente de forma directa.
Solución: usa forwardRef(() => ModuleB) en ambos módulos, o — mejor aún — extrae la dependencia compartida a un tercer módulo.
4. Exponer las Entities Directamente como Respuesta de la API
Problema: campos sensibles (ej. passwordHash) acaban en la respuesta JSON.
Causa: el controller devuelve la entity de TypeORM tal cual, sin una capa DTO/serializer.
Solución: usa @Exclude() de class-transformer en la entity junto con un ClassSerializerInterceptor global, o mejor aún un DTO de respuesta explícito.
5. Contraseñas Guardadas Sin Hashing
Problema: un compromiso de la BD expone todas las contraseñas en texto plano.
Causa: omisión (a menudo durante el rapid prototyping) del paso de hashing.
Solución: hash con bcrypt/argon2 siempre, incluso en desarrollo — nunca hay una razón válida para saltárselo.
6. Secrets JWT Débiles o Hardcodeados en el Código
Problema: un secret predecible permite falsificar tokens válidos.
Causa: un secret por defecto dejado en producción, o commiteado al repositorio.
Solución: secrets generados aleatoriamente (al menos 256 bits), gestionados solo mediante variables de entorno/un secret manager, nunca en el código fuente.
7. Falta de Gestión de Errores Asíncronos en los Services
Problema: las promise rejections no gestionadas provocan un crash del proceso Node.
Causa: llamadas async sin try/catch donde se necesita, u olvidar el exception filter global.
Solución: deja que las excepciones suban hasta el exception filter global (comportamiento por defecto de Nest), intercepta solo donde se necesite un comportamiento distinto (fallback, retry).
8. Queries N+1 No Detectadas
Problema: el endpoint se vuelve extremadamente lento a medida que crecen los datos.
Causa: carga de relaciones dentro de un bucle en lugar de un único join.
Solución: usa relations/leftJoinAndSelect, monitoriza las queries generadas con logging: true en desarrollo.
9. CORS Abierto a Todos en Producción con Credentials
Problema: cualquier sitio web puede hacer peticiones autenticadas a la API en nombre del usuario.
Causa: origin: '*' combinado con credentials: true.
Solución: whitelist explícita de dominios autorizados.
10. Sin Rate Limiting en los Endpoints de Autenticación
Problema: ataques de fuerza bruta al login/registro.
Causa: @nestjs/throttler no aplicado, o con umbrales demasiado permisivos.
Solución: un @Throttle más restrictivo específicamente en login/registro/reset de contraseña.
11. DTO de Update Idéntico al DTO de Create Sin PartialType
Problema: cada PATCH exige todos los campos obligatorios, rompiendo las actualizaciones parciales.
Causa: el DTO de Create se copia sin hacer los campos opcionales.
Solución: class UpdateXDto extends PartialType(CreateXDto) {}.
12. Guards Aplicados Solo a Nivel de Ruta, Nunca de Recurso
Problema: un usuario autenticado puede modificar recursos de otros usuarios.
Causa: confiar solo en @UseGuards(AuthGuard('jwt')) sin comprobación de ownership en el service.
Solución: comprobación explícita resource.ownerId === user.id (o rol admin) en el service, como se muestra en el ejemplo PostsService.update.
13. Conexión a la Base de Datos Sin Pool Configurado Correctamente
Problema: agotamiento de conexiones bajo carga, errores "too many clients".
Causa: valores por defecto del pool no adecuados al tráfico real.
Solución: configura explícitamente extra: { max: 20 } en la connection de TypeORM, calibrado según el plan de la base de datos.
14. Subida de Archivos Sin Límites de Tamaño o Tipo
Problema: un usuario malintencionado sube archivos enormes o ejecutables disfrazados de imágenes.
Causa: FileInterceptor configurado sin limits ni fileFilter.
Solución: establece siempre limits.fileSize y valida el mimetype, como en el ejemplo uploadCover.
15. Variables de Entorno No Validadas al Arrancar
Problema: la app arranca "silenciosamente" con una configuración incompleta y falla de forma críptica más tarde.
Causa: ningún esquema de validación sobre las env vars.
Solución: usa ConfigModule.forRoot({ validationSchema: Joi.object({...}) }) para hacer fallar el bootstrap inmediatamente si falta una variable crítica.
16. Logging de Datos Sensibles
Problema: contraseñas, tokens o datos personales acaban en los logs en texto plano.
Causa: logging indiscriminado de todo el body de la petición.
Solución: redacta (enmascara) explícitamente los campos sensibles antes del logging, o usa los redact paths de pino.
17. Tests Escritos Solo para el "Happy Path"
Problema: los bugs que solo surgen con input no válido o edge cases permanecen invisibles hasta producción.
Causa: presión de tiempo, cobertura de tests limitada al flujo principal.
Solución: testea explícitamente los errores 4xx esperados (validación, permisos, not found), no solo los 2xx.
18. Ausencia de Versionado de la API
Problema: cada cambio breaking rompe a los clientes existentes sin aviso.
Causa: ningún prefijo de versión (/api/v1) o estrategia de versionado de Nest configurada.
Solución: app.setGlobalPrefix('api/v1') desde el primer día, incluso en proyectos pequeños.
19. Dependencias Desactualizadas con Vulnerabilidades Conocidas
Problema: la API sigue expuesta a CVEs públicamente documentadas.
Causa: falta de un proceso de auditoría periódica de dependencias.
Solución: npm audit integrado en la CI, actualizaciones regulares con Dependabot/Renovate.
20. Ninguna Distinción Entre Errores de Validación y Errores de Dominio
Problema: todos los errores vuelven como un 500 genérico, haciendo imposible que el cliente distinga un input erróneo de un problema del servidor.
Causa: uso de Error genérico en lugar de las clases HttpException específicas de Nest.
Solución: usa siempre las excepciones tipadas correctas (BadRequestException, NotFoundException, ForbiddenException, ConflictException...) — así es como Nest comunica el status code HTTP correcto.
Preguntas Frecuentes (FAQ)
1. ¿NestJS es adecuado también para proyectos pequeños?
Sí, pero el verdadero valor aparece cuando el proyecto crece: en un script pequeño, la estructura puede parecer excesiva comparada con Express puro.
2. ¿Tengo que usar TypeScript obligatoriamente?
Técnicamente NestJS también soporta JavaScript puro, pero se pierde gran parte del valor (decoradores tipados, DI type-safe). Está fuertemente desaconsejado en producción.
3. ¿TypeORM o Prisma?
TypeORM se integra de forma más nativa con los decoradores de NestJS y está más maduro en el ecosistema Nest; Prisma ofrece un client generado más type-safe y una mejor developer experience en las migrations, pero requiere una capa de integración manual con Nest.
4. ¿Cómo gestiono las migrations de la base de datos?
Con TypeORM: typeorm migration:generate para generarlas a partir del diff de las entities, typeorm migration:run para aplicarlas en CI/CD, nunca synchronize: true en producción.
5. ¿Cómo estructuro un proyecto muy grande?
Modulariza por dominio (feature modules), no por tipo técnico — evita carpetas globales "controllers/", "services/" que mezclan dominios distintos.
6. ¿Cómo hago testing de los controllers?
Con Test.createTestingModule de @nestjs/testing, mockeando los services inyectados; para los services, mockea los repositories de TypeORM con getRepositoryToken.
7. ¿Cuál es la diferencia entre Guard y Middleware para la autenticación?
El middleware no tiene acceso al contexto de ejecución de Nest (ej. metadatos de los decoradores), por lo que no puede leer @Roles(). Los guards sí — por eso la autenticación/autorización va siempre en los guards, nunca en los middlewares.
8. ¿Puedo usar GraphQL en lugar de REST?
Sí, NestJS tiene soporte oficial tanto para Apollo Server como para Mercurius, con el mismo sistema de módulos/DI — útil si necesitas queries flexibles desde el cliente.
9. ¿Cómo gestiono las transacciones de la base de datos?
Con DataSource.transaction() de TypeORM, o con el decorador @Transactional() de la librería typeorm-transactional para una sintaxis más declarativa.
10. ¿Cómo implemento el refresh token de forma segura?
Guarda un hash del refresh token en el servidor (whitelist), rótalo en cada uso (token rotation), e invalídalo explícitamente en el logout.
11. ¿NestJS soporta microservicios?
Sí, de forma nativa, con transport layers para TCP, Redis, RabbitMQ, Kafka, gRPC y NATS a través de @nestjs/microservices.
12. ¿Cómo hago el deploy a producción?
Build con nest build (compila en dist/), luego node dist/main.js; en plataformas como Railway/Render/Fly.io basta un Dockerfile multi-stage o el buildpack nativo de Node.
13. ¿Cómo gestiono variables de entorno distintas para dev/staging/prod?
ConfigModule.forRoot({ envFilePath: ['.env.${NODE_ENV}', '.env'] }), con los valores reales inyectados por la plataforma de hosting en producción, no desde ficheros .env commiteados.
14. ¿Cómo implemento la subida a S3 en lugar de disco local?
Sustituye el storage engine de Multer por multer-s3, o gestiona la subida manualmente en el service con el SDK de AWS tras recibir el buffer en memoria.
15. ¿Cómo documento automáticamente la API?
Con @nestjs/swagger: decoradores @ApiTags, @ApiProperty en los DTOs, y SwaggerModule.setup() en main.ts generan una UI OpenAPI interactiva en /api/docs.
16. ¿Cómo implemento soft delete?
TypeORM soporta nativamente @DeleteDateColumn() en la entity: softDelete() establece la columna en lugar de eliminar la fila, y las queries excluyen automáticamente los registros eliminados.
17. ¿Cómo gestiono múltiples versiones de la API a la vez?
Con app.enableVersioning({ type: VersioningType.URI }) y el decorador @Version('2') en los controllers/métodos individuales que necesiten coexistir con la v1.
18. ¿Es necesario un ORM o puedo usar queries SQL directas?
Para la mayoría de los casos un ORM reduce errores y boilerplate; para queries muy complejas o críticas en rendimiento, TypeORM permite igualmente queries raw a través de QueryRunner, manteniendo el resto de la app en el ORM.
19. ¿Cómo implemento WebSocket en NestJS?
Con @nestjs/websockets y un @WebSocketGateway(), que se integra con Socket.IO o ws manteniendo el mismo sistema de DI y guards que las rutas REST.
20. ¿Cómo gestiono los cron jobs?
Con @nestjs/schedule y el decorador @Cron('0 0 * * *') en un método de un service — útil para limpieza de datos, envío de digests por email, sincronizaciones periódicas.
21. ¿Cómo protejo la API contra ataques de SQL Injection?
TypeORM/Prisma parametrizan automáticamente las queries — el riesgo solo aparece si construyes queries raw concatenando strings manualmente, algo que siempre debe evitarse.
22. ¿Cuál es la forma correcta de gestionar los secrets en producción?
Nunca en el código o en ficheros commiteados: usa las variables de entorno de la plataforma de hosting o un secret manager dedicado (AWS Secrets Manager, Doppler, Railway Variables).
23. ¿Cómo escalo horizontalmente una API NestJS?
La app debe ser stateless (sin sesiones locales en memoria — usa JWT o Redis para el estado compartido), luego se replica detrás de un load balancer; para WebSocket se necesita un adapter de Redis para sincronizar las conexiones entre instancias.
24. ¿Cómo gestiono la retrocompatibilidad cuando cambio un esquema de respuesta?
Introduce un nuevo campo en lugar de renombrar el existente, deprécialo gradualmente con documentación clara, y usa el versionado de la API para los cambios realmente breaking.
25. ¿Vale la pena escribir tests E2E además de los unit tests para una API NestJS?
Sí — @nestjs/testing con supertest permite testear toda la pipeline (incluyendo guards, pipes, interceptors) llamando a los endpoints reales sobre una instancia de la app en memoria, capturando problemas de integración que los unit tests por sí solos no ven.
Recursos y Documentación Oficial
Para profundizar en cada tema tratado en esta guía, la documentación oficial sigue siendo la fuente más fiable y actualizada:
- NestJS — documentación oficial:
docs.nestjs.com, en particular las secciones sobre Guards, Interceptors, Pipes y Exception Filters. - TypeORM — documentación oficial:
typeorm.io, para relaciones, migrations y el query builder avanzado. - Passport.js:
passportjs.org, la librería de autenticación en la que se basa@nestjs/passport. - class-validator: repositorio oficial en GitHub, con la lista completa de decoradores de validación disponibles más allá de los usados en esta guía.
- OWASP API Security Top 10: la checklist de referencia para la seguridad de APIs REST, complementaria a la sección de Seguridad de esta guía.
- jwt.io: herramienta para inspeccionar y decodificar tokens JWT durante el debug de la autenticación.
Artículos Relacionados Recomendados
Esta guía cubre todo el ciclo de vida de una REST API NestJS, pero cada sección puede convertirse en una profundización aparte. Estos son los temas naturalmente relacionados que vale la pena explorar a continuación:
- Modularizar un proyecto NestJS de gran tamaño: feature modules, shared modules y barrel files.
- Dependency Injection en NestJS explicada con ejemplos: providers personalizados, factory providers, tokens de inyección.
- Autenticación JWT y Refresh Token en profundidad: token rotation, revocación y gestión multi-dispositivo.
- Role Based Access Control (RBAC) avanzado: permisos granulares más allá de los roles simples.
- Validación con ValidationPipe y class-validator: validadores personalizados y mensajes de error localizados.
- Logging centralizado en NestJS con Pino y correlación de peticiones mediante request ID.
- Exception Filters personalizados para dominios específicos (ej. errores de pago, errores de dominio).
- Subida de archivos avanzada con Multer: almacenamiento en S3, validación del contenido real de los archivos.
- Scheduler y Cron Jobs en NestJS con
@nestjs/schedule. - WebSocket con NestJS: notificaciones en tiempo real y escalado con el adapter de Redis.
- Testing de controllers y services NestJS: unit tests, mock de repositories, tests E2E con Supertest.
Conclusión
Construir una REST API con NestJS significa invertir en una estructura que escala con la complejidad del proyecto: módulos bien aislados, Dependency Injection para la testabilidad, guards/interceptors/pipes para separar los cross-cutting concerns de la lógica de negocio, y un ecosistema — TypeORM, Passport, class-validator, Swagger — que cubre de forma madura cualquier necesidad real de producción. El ejemplo de API de Blog construido en esta guía (CRUD, JWT con refresh token, RBAC, subida de archivos, logging centralizado, gestión de errores) es el esqueleto reutilizable para la mayoría de las APIs NestJS que encontrarás en la práctica.
Próximos pasos recomendados: profundiza en la modularización de proyectos NestJS de gran tamaño, la autenticación JWT y refresh token en detalle, el patrón RBAC con permisos granulares, y las técnicas de testing para controllers y services — temas que completan naturalmente esta guía.