🎯 Meta: aprender el framework backend más estructurado del ecosistema JavaScript. NestJS te obliga a organizar el código con módulos, inyección de dependencias y patrones. Es "el Laravel/Spring de Node": ideal para aplicaciones grandes y equipos.
Versiones: Node.js 24 LTS · NestJS 11 · TypeScript 7 · Prisma / TypeORM.
📘 ¿No dominas TypeScript? Lee primero el Apéndice A. Y actualiza a TypeScript 7 (compilador nativo en Go, GA jul-2026): compila 8-12× más rápido sin cambiar tu código.
6.1 · Primero: entender Node y TypeScript
Node.js es JavaScript ejecutándose fuera del navegador (en el servidor). Su modelo es asíncrono y de un solo hilo con event loop: no se bloquea esperando I/O, atiende miles de conexiones. Por eso brilla en APIs y tiempo real.
TypeScript es JavaScript + tipos. Detecta errores antes de ejecutar y hace el código mantenible. En backend serio, TypeScript es el estándar de facto en 2026.
🧠 Node "crudo" vs framework: podrías hacer una API con el módulo
httpde Node, pero tendrías que resolver rutas, validación, DI, etc. tú mismo (como Flask, pero peor). NestJS te da una arquitectura completa y opinada. Es el otro extremo de Elysia (cap. 07), que es minimalista.
6.2 · Instalación
node --version # 24.x (LTS)
npm i -g @nestjs/cli
nest new tienda-api # elige npm/pnpm cuando pregunte
cd tienda-api
npm run start:dev # arranca con recarga en http://localhost:30006.3 · Los 3 bloques de NestJS
Todo en NestJS gira en torno a tres piezas. Entiéndelas y entiendes el framework entero:
┌──────────────────────────────────────────────────────────────┐
│ CONTROLLER → recibe la petición HTTP, extrae datos, │
│ llama al service y devuelve la respuesta. │
│ (NO lleva lógica de negocio) │
├──────────────────────────────────────────────────────────────┤
│ SERVICE → la lógica de negocio: valida reglas, habla │
│ con la BD. Es "inyectable" (@Injectable). │
├──────────────────────────────────────────────────────────────┤
│ MODULE → agrupa controllers + services relacionados y │
│ declara qué expone e importa. Organiza la app. │
└──────────────────────────────────────────────────────────────┘Genera un CRUD completo con un comando (crea las 3 piezas + DTOs + tests):
nest g resource productos # elige "REST API" y "yes" a los endpoints CRUD6.4 · Controller
// src/productos/productos.controller.ts
import {
Controller, Get, Post, Put, Delete, Body, Param, ParseIntPipe, HttpCode,
} from '@nestjs/common';
import { ProductosService } from './productos.service';
import { CrearProductoDto } from './dto/crear-producto.dto';
@Controller('productos') // prefijo de ruta: /productos
export class ProductosController {
// Inyección de dependencias: Nest crea y pasa el service solo
constructor(private readonly productos: ProductosService) {}
@Get()
listar() {
return this.productos.listar();
}
@Get(':id')
ver(@Param('id', ParseIntPipe) id: number) { // ParseIntPipe valida que sea número
return this.productos.buscar(id);
}
@Post()
crear(@Body() dto: CrearProductoDto) { // @Body inyecta y valida el cuerpo
return this.productos.crear(dto);
}
@Put(':id')
editar(@Param('id', ParseIntPipe) id: number, @Body() dto: CrearProductoDto) {
return this.productos.editar(id, dto);
}
@Delete(':id')
@HttpCode(204)
borrar(@Param('id', ParseIntPipe) id: number) {
return this.productos.borrar(id);
}
}🧠 Decoradores (
@Controller,@Get,@Body…): son "etiquetas" que añaden comportamiento.@Get(':id')dice "esta función maneja GET /productos/:id". Es el mismo concepto que@app.routede Flask o#[Route]de otros, pero muy pulido.
6.5 · DTOs y validación
Un DTO (Data Transfer Object) define y valida la forma de los datos que entran. Se usa class-validator:
npm i class-validator class-transformer// src/productos/dto/crear-producto.dto.ts
import { IsString, IsNumber, IsInt, Min, MaxLength, IsOptional } from 'class-validator';
export class CrearProductoDto {
@IsString()
@MaxLength(255)
nombre: string;
@IsNumber()
@Min(0)
precio: number;
@IsInt()
@Min(0)
@IsOptional()
stock?: number;
}Activa la validación global en main.ts:
// src/main.ts
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // elimina campos no declarados en el DTO
forbidNonWhitelisted: true, // rechaza si mandan campos de más
transform: true, // convierte tipos automáticamente
}));
await app.listen(3000);
}
bootstrap();Ahora cualquier dato inválido devuelve 400 con detalle, automáticamente.
💡 Tip de seguridad:
whitelist: true+forbidNonWhitelisted: truees una defensa importante: aunque un atacante mande{ "es_admin": true }, el DTO lo descarta. Igual que separar modelos entrada/salida en FastAPI.
6.6 · Service (lógica de negocio)
// src/productos/productos.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CrearProductoDto } from './dto/crear-producto.dto';
@Injectable()
export class ProductosService {
private items = new Map<number, any>();
private seq = 0;
listar() {
return [...this.items.values()];
}
buscar(id: number) {
const p = this.items.get(id);
if (!p) throw new NotFoundException(`Producto ${id} no existe`); // → 404 solo
return p;
}
crear(dto: CrearProductoDto) {
const producto = { id: ++this.seq, ...dto, disponible: (dto.stock ?? 0) > 0 };
this.items.set(producto.id, producto);
return producto;
}
editar(id: number, dto: CrearProductoDto) {
const p = this.buscar(id);
Object.assign(p, dto);
return p;
}
borrar(id: number) {
this.buscar(id); // lanza 404 si no existe
this.items.delete(id);
}
}🧠 Excepciones semánticas:
throw new NotFoundException()→ Nest responde 404 con JSON automáticamente. Hay una para cada caso:BadRequestException(400),UnauthorizedException(401),ForbiddenException(403),ConflictException(409). No armas el response a mano.
6.7 · Base de datos con Prisma
El ORM más popular con NestJS en 2026 es Prisma (tipado, migraciones, excelente DX):
npm i prisma @prisma/client
npx prisma init --datasource-provider postgresqlDefine el esquema en prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Producto {
id Int @id @default(autoincrement())
nombre String
precio Decimal @db.Decimal(10, 2)
stock Int @default(0)
activo Boolean @default(true)
creadoEn DateTime @default(now()) @map("creado_en")
@@map("productos")
}npx prisma migrate dev --name init # crea la migración y la aplicaUn servicio Prisma inyectable:
// src/prisma.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
}Y lo usas en el service (fíjate en el tipado total):
@Injectable()
export class ProductosService {
constructor(private prisma: PrismaService) {}
listar() {
return this.prisma.producto.findMany({ orderBy: { creadoEn: 'desc' } });
}
async buscar(id: number) {
const p = await this.prisma.producto.findUnique({ where: { id } });
if (!p) throw new NotFoundException(`Producto ${id} no existe`);
return p;
}
crear(dto: CrearProductoDto) {
return this.prisma.producto.create({ data: dto });
}
}💡 Prisma vs TypeORM: Prisma tiene mejor experiencia y tipado; TypeORM se parece más a Eloquent (patrón Active Record) y se integra "más Nest". Para empezar, Prisma.
6.8 · Módulo (el pegamento)
// src/productos/productos.module.ts
import { Module } from '@nestjs/common';
import { ProductosController } from './productos.controller';
import { ProductosService } from './productos.service';
import { PrismaService } from '../prisma.service';
@Module({
controllers: [ProductosController],
providers: [ProductosService, PrismaService],
exports: [ProductosService], // para que otros módulos lo usen
})
export class ProductosModule {}Y se importa en el módulo raíz AppModule. Así se compone la aplicación: módulos que importan módulos. Escala a cientos de features sin caos.
6.9 · Autenticación (JWT + Guards)
NestJS usa Guards para proteger rutas (deciden si una petición pasa o no):
npm i @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt// auth.guard.ts (simplificado)
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwt: JwtService) {}
async canActivate(ctx: ExecutionContext): Promise<boolean> {
const req = ctx.switchToHttp().getRequest();
const token = req.headers.authorization?.split(' ')[1];
if (!token) throw new UnauthorizedException();
try {
req.user = await this.jwt.verifyAsync(token, { secret: process.env.JWT_SECRET });
return true;
} catch {
throw new UnauthorizedException('Token inválido');
}
}
}// Proteger un endpoint:
@UseGuards(AuthGuard)
@Post()
crear(@Body() dto: CrearProductoDto) { ... }🧠 Guard vs Middleware vs Pipe vs Interceptor: NestJS tiene una "tubería" ordenada: Middleware (antes de todo) → Guard (¿puede pasar? auth/permisos) → Pipe (validar/transformar datos) → Controller → Interceptor (envolver la respuesta, logging). Cada uno tiene su rol. Esta separación limpia es la razón de existir de NestJS.
6.9.1 · Interceptors y Exception Filters
Un Interceptor envuelve la ejecución del handler: puede transformar la respuesta, medir tiempos o cachear, corriendo tanto antes como después del controller (a diferencia del Guard, que solo decide sí/no antes):
// logging.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler): Observable<any> {
const inicio = Date.now();
const req = ctx.switchToHttp().getRequest();
return next.handle().pipe( // next.handle() ejecuta el controller
tap(() => console.log(`${req.method} ${req.url} — ${Date.now() - inicio}ms`)),
);
}
}// Envolver TODAS las respuestas en { data, timestamp } — un formato uniforme:
@Injectable()
export class RespuestaInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler) {
return next.handle().pipe(map(data => ({ data, timestamp: new Date().toISOString() })));
}
}
// Aplícalo global en main.ts:
app.useGlobalInterceptors(new LoggingInterceptor(), new RespuestaInterceptor());Un Exception Filter hace lo opuesto: captura errores y decide la respuesta, útil para dar un formato de error consistente en toda la API (o traducir excepciones de Prisma a HTTP):
// prisma-exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, ConflictException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
@Catch(Prisma.PrismaClientKnownRequestError)
export class PrismaExceptionFilter implements ExceptionFilter {
catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) {
if (exception.code === 'P2002') { // violación de constraint único
const res = host.switchToHttp().getResponse();
return res.status(409).json({ message: 'Ese registro ya existe' });
}
throw exception;
}
}🧠 Orden completo de la tubería de Nest (amplía el diagrama de 6.9): Middleware → Guard → Interceptor (parte "antes") → Pipe → Controller → Interceptor (parte "después") → Exception Filter (solo si algo lanzó una excepción). Cada capa tiene una responsabilidad y ninguna conoce la siguiente — el mismo principio de capas del capítulo 11.
6.9.2 · Colas de trabajo con BullMQ
Igual que Flask, NestJS no trae colas de fábrica. La librería estándar es BullMQ (sobre Redis, apéndice B):
npm i @nestjs/bullmq bullmq// productos.module.ts
import { BullModule } from '@nestjs/bullmq';
@Module({
imports: [BullModule.registerQueue({ name: 'notificaciones' })],
// ...
})
export class ProductosModule {}// productos.service.ts — encolar en vez de bloquear la petición
import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bullmq';
@Injectable()
export class ProductosService {
constructor(@InjectQueue('notificaciones') private cola: Queue) {}
async crear(dto: CrearProductoDto) {
const producto = await this.prisma.producto.create({ data: dto });
await this.cola.add('producto-creado', { id: producto.id }, {
attempts: 3, // reintentos
backoff: { type: 'exponential', delay: 5000 },
});
return producto;
}
}// notificaciones.processor.ts — el worker que procesa la cola
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq';
@Processor('notificaciones')
export class NotificacionesProcessor extends WorkerHost {
async process(job: Job) {
if (job.name === 'producto-creado') {
// enviar email, avisar a un webhook, etc. — el mismo principio de idempotencia
// que en Laravel (3.14) y Flask (4.13): reintentos = puede ejecutarse más de una vez.
}
}
}6.10 · Tests (Jest — unit + e2e)
NestJS trae Jest configurado y genera tests con cada recurso.
Test unitario del service (con dependencias simuladas):
// productos.service.spec.ts
import { Test } from '@nestjs/testing';
import { ProductosService } from './productos.service';
import { PrismaService } from '../prisma.service';
describe('ProductosService', () => {
let service: ProductosService;
const prismaMock = {
producto: { create: jest.fn(dto => ({ id: 1, ...dto.data })) },
};
beforeEach(async () => {
const modulo = await Test.createTestingModule({
providers: [
ProductosService,
{ provide: PrismaService, useValue: prismaMock }, // inyecta el mock
],
}).compile();
service = modulo.get(ProductosService);
});
it('crea un producto', () => {
const r = service.crear({ nombre: 'Teclado', precio: 99 });
expect(r).resolves.toHaveProperty('id', 1);
});
});Test e2e (petición HTTP real contra la app):
// test/productos.e2e-spec.ts
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('Productos (e2e)', () => {
let app;
beforeAll(async () => {
const modulo = await Test.createTestingModule({ imports: [AppModule] }).compile();
app = modulo.createNestApplication();
await app.init();
});
it('POST /productos crea', () => {
return request(app.getHttpServer())
.post('/productos')
.send({ nombre: 'Mouse', precio: 49.9 })
.expect(201);
});
it('POST /productos sin nombre → 400', () => {
return request(app.getHttpServer())
.post('/productos').send({ precio: 10 }).expect(400);
});
afterAll(() => app.close());
});npm run test # unitarios
npm run test:e2e # end to end
npm run test:cov # cobertura💡 Tip: el
Test.createTestingModulees DI para tests: reemplazas cualquier dependencia por un mock conuseValue. Igual filosofía quedependency_overridesde FastAPI. Testear es fácil porque usaste inyección de dependencias.
6.11 · Buenas prácticas NestJS
- Controllers finos, services con la lógica. El controller solo traduce HTTP ↔ service.
- Un módulo por feature. Nada de un módulo gigante.
- DTOs con
class-validator+ValidationPipeglobal (whitelist activado). - Excepciones de Nest (
NotFoundException…), no armes responses a mano. - Config con
@nestjs/configy.envvalidado por esquema. - Interceptors para logging y formato de respuesta uniforme.
- Swagger gratis:
@nestjs/swaggergenera/docscomo FastAPI.
✅ Ejercicio del capítulo
La API del blog en NestJS con arquitectura completa:
1. Módulos: articulos, comentarios, auth.
2. Prisma con relación Articulo↔Comentario↔User.
3. DTOs validados + ValidationPipe global.
4. Services con toda la lógica; controllers finos.
5. AuthGuard JWT protegiendo escritura.
6. Tests: unit del service (con mock de Prisma) + e2e de los endpoints.
7. Añade Swagger y revisa /api (docs).
8. Añade un Interceptor de logging global que mida cuánto tarda cada petición.
9. Encola con BullMQ el envío de notificación al crear un comentario.Ya dominas el framework más estructurado. Ahora, el contrapunto: Elysia, minimalista y rapidísimo sobre Bun.
💡 Pistas de la solución
- La relación Articulo↔Comentario↔User en Prisma:
ComentariotienearticuloIdyautorIdcomo claves foráneas; en elschema.prisma, cada relación necesita el campo Y su contraparte@relationen el otro modelo. - El
AuthGuardJWT protege rutas de escritura con@UseGuards(AuthGuard)en el controller o método — pero recuerda que elJwtModuledebe registrarse con el mismosecretque firma el token en el login. - Para mockear Prisma en tests unitarios de servicio:
useValue: { producto: { findMany: jest.fn() } }en elprovidersdelTestingModule— no necesitas una BD real para testear la lógica del service.
🧠 Autoevaluación
Porque el controller es el punto de entrada HTTP (traduce petición↔respuesta) y no debería saber nada del dominio; la lógica de negocio vive en el service para poder testearla sin levantar un servidor HTTP, y para poder reutilizarla desde otro controller o un job en segundo plano sin duplicar código.
Testeabilidad y desacoplamiento: en Test.createTestingModule, cualquier dependencia se sustituye por un mock con useValue sin tocar la clase que la usa. Es el mismo principio de inversión de dependencias que viste con Depends en FastAPI (cap. 05) — cambia la sintaxis, no la idea.
Porque descarta automáticamente cualquier campo del body que no esté declarado en el DTO — sin esto, un atacante podría mandar campos extra (como esAdmin: true) que tu código podría usar sin querer si en algún punto haces un ...body sin filtrar.
Que el secret usado para firmar el JWT en el login sea EXACTAMENTE el mismo que el configurado en el JwtModule/estrategia que verifica el guard — un secret distinto (o leído de una variable de entorno distinta) hace que la verificación falle o, peor, que un guard mal configurado no se ejecute en absoluto.
El Guard solo decide sí/no antes de que se ejecute el controller (autenticación/autorización); el Interceptor envuelve la ejecución completa y puede actuar tanto antes como después — transformar la respuesta, medir tiempos o cachear resultados una vez el controller ya respondió.
Porque BullMQ reintenta jobs fallidos (attempts/backoff), y un reintento vuelve a ejecutar el process() completo — si el efecto (enviar un email, cobrar) no es idempotente, un reintento tras un fallo parcial lo duplicaría. Es el mismo principio de idempotencia de colas que en Laravel (3.14) y Flask (4.13).
Siguiente: 07-bun-elysia.md — el runtime más rápido de JavaScript.