<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Crear una REST API completa con Nestjs: La guía definitiva sobre Typeorm, JWT, RBAC y seguridad

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

VentajasDesventajas
Estructura clara y consistente entre proyectos distintosCurva de aprendizaje más pronunciada que Express puro (decoradores, DI, módulos)
Testabilidad nativa gracias al DIOverhead de boilerplate en proyectos muy pequeños
Ecosistema oficial amplio y bien mantenidoMás "magia" (decoradores, reflection) frente a código explícito
Excelente integración con TypeScript y Swagger/OpenAPIBundle size y cold start ligeramente superiores a frameworks minimalistas (relevante en entornos serverless)
Facilita adoptar Clean Architecture / hexagonalRequiere 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).

HerramientaVersión recomendadaPara qué sirve
Node.js20.x LTS o superiorRuntime JavaScript sobre el que corre NestJS
npm10.x (incluido en Node 20)Gestión de paquetes
NestJS CLI@nestjs/cli 10.x+Scaffolding de módulos, controllers, services
TypeScript5.xLenguaje en el que están escritos NestJS y tu app
VS CodeÚltima versión estableEditor con soporte nativo para TypeScript
PostgreSQL15.x o superiorBase 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

PrincipioAplicación Práctica en NestJS
Clean ArchitectureSeparació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 ResponsibilityUn controller mapea peticiones HTTP, un service contiene una sola área de lógica de negocio, un repository un solo aggregate.
SOLID — Dependency InversionLos services dependen de interfaces/tokens abstractos (ej. @InjectRepository), no de implementaciones concretas — sustituibles por mocks en los tests.
DRYLógica de validación compartida en los DTOs con PartialType/PickType/OmitType, evitando duplicación entre el DTO de Create y el de Update.
KISSEvita introducir CQRS, Event Sourcing o microservicios hasta que la complejidad real del dominio lo justifique.
Repository PatternTypeORM/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.
DTOCada endpoint tiene un DTO de entrada dedicado — nunca exponer directamente las entities de la base de datos como contrato de API.
ValidationValidationPipe 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 select explícito en TypeORM para evitar transferir columnas innecesarias (ej. excluir content en las listas).
  • Añade índices a las columnas usadas con frecuencia en WHERE/ORDER BY (ver el @Index en el slug de la entity Post).
  • Evita el problema N+1: usa relations o QueryBuilder con leftJoinAndSelect en 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

MedidaCómo se Implementa
JWTAccess token de vida corta firmado con un secret robusto (32+ caracteres aleatorios), nunca hardcodeado en el código.
Refresh TokenVida más larga, idealmente revocable (whitelist en Redis/DB), rotado en cada uso.
Hash de Contraseña / bcryptbcrypt.hash(password, 12) — nunca MD5/SHA1, nunca contraseñas en texto plano en la BD.
Helmetapp.use(helmet()) establece headers de seguridad (CSP, X-Frame-Options, HSTS) en una sola línea.
CORSapp.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.
ValidationPipewhitelist + forbidNonWhitelisted para bloquear mass assignment en campos no previstos.
Sanitización de Inputclass-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.

💬 Notas de los lectores

0 notas

Escribe una nota

Comparte tu opinión, una sugerencia o un cumplido

Últimas notas

Aún no hay notas. ¡Sé el primero en comentar!