NestJS 11 + Express 5: Lo que rompe tu API y cómo arreglarlo

igración a NestJS 11 con Express 5 — arreglos y breaking changes en APIs

Introducción

Eran las 3:00 AM cuando tu monitor de producción lanzó la primera alerta roja. Tu API, que funcionaba perfectamente en NestJS 10, comenzó a devolver 404 Not Found para todas las rutas con wildcards después de la actualización automática. No estás solo en esta situación: más del 60% de los desarrolladores que migraron a NestJS 11 reportaron problemas iniciales relacionados con el cambio Express 4 → Express 5.

NestJS 11, lanzado en enero de 2025, representa el salto más significativo desde la versión 9.0.0, no solo por las mejoras en rendimiento (startup time reducido hasta 40% en aplicaciones con muchos módulos dinámicos), sino porque ahora Express 5 es el adaptador HTTP por defecto. Express 5 introduce cambios rupturistas en cómo se definen las rutas, manejan middlewares y procesan errores que pueden dejar tu API inoperativa si no estás preparado.

En este artículo, exploraremos en profundidad cada breaking change, con 10 ejemplos de código real mostrando el antes/después, estrategias de migración progresiva, y cómo aprovechar las nuevas características como JSON logging nativo y mejor manejo de errores asíncronos. Al finalizar, tendrás un plan claro para migrar tu aplicación sin interrumpir el servicio y podrás aprovechar las mejoras de rendimiento de Express 5.

Advertencia: Este artículo asume que tienes experiencia intermedia-avanzada con NestJS, comprendes los conceptos de decoradores, inyección de dependencias y middlewares en Express/NestJS.

Prerrequisitos

Conocimientos Mínimos:

– TypeScript avanzado (decorators, generics, utility types)

– Node.js 18+ (requerido para Express 5)

– NestJS fundamentals: Modules, Controllers, Providers, Guards, Interceptors

– Express middleware patterns y request lifecycle

Stack Tecnológico Requerido:

– Node.js 22.1.0+ (recomendado: LTS más reciente)

– npm 10+ o pnpm 8+

– NestJS 10.x actual (desde el cual migraremos)

– TypeScript 5.6+

– Git para control de versiones durante la migración

Herramientas Necesarias:

# Herramientas esenciales de diagnóstico
npm install -g @nestjs/cli
npm install -g typescript
npm install -g @expressjs/codemod  # Para migración automática

Tiempo Estimado:

Lectura: 25-30 minutos

Migración práctica: 2-4 horas (dependiendo del tamaño de la aplicación)

Testing completo: 1-2 horas adicionales

La Arquitectura de NestJS 11 + Express 5

Antes de profundizar en los breaking changes, entendamos qué cambió en la arquitectura interna y por qué estos cambios son necesarios.

[Diagrama: Arquitectura NestJS 11 vs NestJS 10]

NestJS 10                          NestJS 11
┌─────────────────┐               ┌─────────────────┐
│  NestJS Core    │               │  NestJS Core    │
│  (v10.x)        │               │  (v11.x)        │
└────────┬────────┘               └────────┬────────┘
         │                                  │
    ┌────▼────┐                        ┌────▼────┐
    │ Express │                        │ Express │
    │   v4    │  ← ADAPTER             │   v5    │  ← ADAPTER
    └─────────┘                        └─────────┘
         │                                  │
    ┌────▼────┐                        ┌────▼────┐
    │ Router  │                        │ Router  │
    │ Legacy  │                        │ New API │
    └─────────┘                        └─────────┘

Mejoras Clave en Express 5

Express 5 no es simplemente una actualización de mantenimiento: representa 5 años de desarrollo desde Express 4.0.0 (2014) con mejoras arquitectónicas significativas:

1. Algoritmo de Route Matching Rediseñado: El nuevo algoritmo es 30% más rápido en rutas complejas con múltiples parámetros y wildcards.

2. Error Handling Asíncrono Nativo: Express 5 ahora captura automáticamente promesas rechazadas en middlewares y rutas, eliminando la necesidad de `try-catch` manual en cada función async.

3. Soporte Brotli: Compresión automática mejorada sin necesidad de middleware adicional.

4. TypeScript Definitions Mejoradas: Types más precisos que previenen errores en tiempo de desarrollo.

Por Qué NestJS Cambió a Express 5

La decisión de hacer Express 5 el default en NestJS 11 no fue arbitraria. Los benchmarks de Trilon (empresa consultora detrás de NestJS) mostraron:

15-20% mejor throughput en endpoints con body parsing

40% reducción en memory footprint para aplicaciones con muchos routes

Startup time 25% menor en aplicaciones modulares grandes

> 💡 Pro Tip: NestJS 11 también soporta Fastify v5. Si necesitas máximo rendimiento y no dependes de middlewares específicos de Express, considera migrar a Fastify.

Breaking Change #1: Wildcard Routing Syntax

Este es el cambio más impactante que romperá tu API inmediatamente después de la migración si utilizas wildcards en tus rutas.

El Problema

En Express 4 y NestJS 10, podías usar wildcards de forma simple:

// ❌ NESTJS 10 - YA NO FUNCIONA EN v11
@Controller('api')
export class ApiController {
  @Get('docs/*')
  findAllDocumentation() {
    return 'Documentación API';
  }

  @Get('files/:file.:ext?')
  getFile() {
    return 'Archivo';
  }
}

En NestJS 11 con Express 5, estas rutas ahora se comportan diferente:

1. `/*` (wildcard sin nombre) está deprecado y puede no funcionar

2. El carácter `?` para parámetros opcionales ya no es soportado

3. Los caracteres regexp `[` `]` `(` `)` `?` `!` están reservados

La Solución

Express 5 introduce una sintaxis más estricta y explícita:

1. Wildcards Deben Tener Nombre

// ✅ NESTJS 11 - CORRECTO
@Controller('api')
export class ApiController {
  // Opción A: Wildcard con nombre (no incluye root path)
  @Get('docs/*splat')
  findAllDocumentation(@Param('0') path: string) {
    return `Documentación para: ${path}`;
    // GET /api/docs/auth/login → path = "auth/login"
  }

  // Opción B: Wildcard opcional con llaves (incluye root path)
  @Get('files/{*splat}')
  findAllFiles(@Param('0') path: string) {
    return `Archivos en: ${path || 'root'}`;
    // GET /api/files → path = "" (empty string)
    // GET /api/docs/auth → path = "auth"
  }
}

> ⚠️ Warning: El nombre `splat` es arbitrario. Puedes usar `path`, `wildcard`, `*rest`, etc. NestJS/Express solo requieren que tenga un nombre.

2. Parámetros Opcionales con Llaves

// ❌ NESTJS 10 - Carácter '?' deprecado
@Controller('assets')
export class AssetsController {
  @Get(':file.:ext?')
  getAsset(
    @Param('file') file: string,
    @Param('ext') ext?: string
  ) {
    return ext ? `${file}.${ext}` : file;
  }
}

// ✅ NESTJS 11 - Sintaxis con llaves
@Controller('assets')
export class AssetsController {
  @Get(':file{.:ext}')
  getAsset(
    @Param('file') file: string,
    @Param('ext') ext?: string
  ) {
    // Ahora 'ext' puede ser undefined
    return ext ? `${file}.${ext}` : file;
  }
}

Caso Real: Migración de Rutas Complejas

Considera una aplicación real de e-commerce con rutas para productos y categorías:

// ❌ ANTES (NestJS 10) - Múltiples problemas
@Controller('products')
export class ProductsController {
  @Get('[new|featured]/:slug')
  getProductByType(@Param('slug') slug: string) {
    return this.productsService.findBySlug(slug);
  }

  @Get('category/*subcategories')
  getCategoryProducts(@Param('0') path: string) {
    const categories = path.split('/');
    return this.productsService.findByCategory(categories);
  }

  @Get(':id{.:format}?')
  getProduct(@Param('id') id: string, @Param('format') format?: string) {
    return this.productsService.findById(id, format);
  }
}

// ✅ DESPUÉS (NestJS 11) - Todas las rutas corregidas
@Controller('products')
export class ProductsController {
  // Regexp characters reemplazados por array de rutas
  @Get(['new/:slug', 'featured/:slug'])
  getProductByType(@Param('slug') slug: string) {
    return this.productsService.findBySlug(slug);
  }

  // Wildcard con nombre explícito
  @Get('category/*subcategories')
  getCategoryProducts(
    @Param('subcategories') subcategories: string
  ) {
    const categories = subcategories.split('/');
    return this.productsService.findByCategory(categories);
  }

  // Parámetro opcional con sintaxis correcta
  // Nota: El '?' fue removido, las llaves definen opcionalidad
  @Get(':id{.:format}')
  getProduct(
    @Param('id') id: string,
    @Param('format') format?: string
  ) {
    return this.productsService.findById(id, format);
  }
}

Script de Migración Automática

NestJS 11 incluye un helper interno que convierte automáticamente wildcards sin nombre a wildcards con nombre:

// NestJS convierte internamente:
@Get('users/*')

// A:
@Get('users/*0')  // Genera nombre automáticamente

Sin embargo, no confíes 100% en esta conversión automática. Es mejor actualizar las rutas explícitamente para mantener claridad en tu código.

Breaking Change #2: Error Handling en Middlewares Asíncronos

Este cambio es menos visible pero más peligroso: puede causar que errores asíncronos no se capturen correctamente.

El Problema en Express 4/NestJS 10

En versiones anteriores, las promesas rechazadas en middlewares async no siempre se propagaban correctamente al error handler:

// ❌ NESTJS 10 - Error handling inconsistente
@Injectable()
export class AsyncValidationMiddleware implements NestMiddleware {
  async use(req: any, res: any, next: () => void) {
    // Si esta promesa se rechaza, Express 4 puede no manejarlo bien
    const user = await this.validateUser(req.headers.authorization);
    req.user = user;
    next();
  }

  private async validateUser(token: string) {
    if (!token) {
      // Este rejection puede no ser capturado
      throw new UnauthorizedException();
    }
    return this.usersService.findByToken(token);
  }
}

La Mejora en Express 5

Express 5 ahora captura automáticamente promesas rechazadas y las pasa al error handler:

// ✅ NESTJS 11 - Error handling consistente
@Injectable()
export class AsyncValidationMiddleware implements NestMiddleware {
  async use(req: any, res: any, next: () => void) {
    // Promesas rechazadas se manejan automáticamente
    const user = await this.validateUser(req.headers.authorization);
    req.user = user;
    next();
  }

  private async validateUser(token: string) {
    if (!token) {
      // Express 5 captura esto y llama a next(err)
      throw new UnauthorizedException();
    }
    return this.usersService.findByToken(token);
  }
}

// Error handler recibe el error automáticamente
@Catch(UnauthorizedException)
export class UnauthorizedFilter implements ExceptionFilter {
  catch(exception: UnauthorizedException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = HttpStatus.UNAUTHORIZED;

    response.status(status).json({
      statusCode: status,
      message: exception.message,
      timestamp: new Date().toISOString(),
    });
  }
}

Caso Real: API Gateway con Múltiples Services

Imagina un API Gateway que valida tokens con múltiples servicios externos:

// ✅ NESTJS 11 - Error handling robusto
@Injectable()
export class GatewayMiddleware implements NestMiddleware {
  constructor(
    @Inject('AUTH_SERVICE') private authService: ClientProxy,
    @Inject('USER_SERVICE') private userService: ClientProxy,
  ) {}

  async use(req: any, res: any, next: () => void) {
    try {
      // Todas estas promesas son manejadas correctamente
      const token = this.extractToken(req);
      const authResponse = await this.authService.send(
        { cmd: 'validate_token' },
        { token }
      ).toPromise();

      if (!authResponse.valid) {
        throw new UnauthorizedException('Invalid token');
      }

      const user = await this.userService.send(
        { cmd: 'get_user' },
        { userId: authResponse.userId }
      ).toPromise();

      req.user = user;
      next();
    } catch (error) {
      // Express 5 captura cualquier throw o rejected promise
      // y lo pasa al error handler de NestJS
      throw error;
    }
  }

  private extractToken(req: Request): string {
    const authHeader = req.headers.authorization;
    if (!authHeader) {
      throw new UnauthorizedException('No token provided');
    }
    return authHeader.split(' ')[1];
  }
}

> ✅ Best Practice: En NestJS 11, ya NO necesitas envolver middlewares async en try-catch para capturar errores. Express 5 lo hace automáticamente. Solo usa try-catch si necesitas transformar el error antes de lanzarlo.

Breaking Change #3: Métodos Deprecados de Express

Express 5 remueve definitivamente métodos que estaban deprecados desde Express 4. Si tu código usa alguno de estos, crashearán en runtime.

Métodos Removidos Complejos

1. `app.del()` → `app.delete()`

// ❌ v4 - Método deprecado
app.del('/user/:id', (req, res) => {
  res.send(`DELETE /user/${req.params.id}`);
});

// ✅ v5 - Método correcto
app.delete('/user/:id', (req, res) => {
  res.send(`DELETE /user/${req.params.id}`);
});

En NestJS esto se traduce en decoradores:

// ❌ INCORRECTO - No uses verbos deprecated
@Controller('users')
export class UsersController {
  // NestJS nunca tuvo 'del', pero si usas custom adapters...
}

// ✅ CORRECTO
@Controller('users')
export class UsersController {
  @Delete(':id')
  deleteUser(@Param('id') id: string) {
    return this.usersService.remove(id);
  }
}

2. `res.json(obj, status)` → `res.status(status).json(obj)`

// ❌ v4 - Firma deprecada
@Post('users')
createUser(@Body() createUserDto: CreateUserDto) {
  return this.usersService.create(createUserDto);
}

// En el controller method (si usas Response directamente):
res.json({ name: 'John' }, 201);

// ✅ v5 - Firma correcta
@Post('users')
@HttpCode(201)
createUser(@Body() createUserDto: CreateUserDto) {
  return this.usersService.create(createUserDto);
}

// Si necesitas usar Response directamente:
res.status(201).json({ name: 'John' });

3. `res.send(status)` → `res.sendStatus(statusCode)`

// ❌ v4
res.send(200);  // Ambiguous: ¿es status o body?

// ✅ v5
res.sendStatus(200);  // Explícito: envía status + text
res.status(200).send({ message: 'OK' });  // Status + JSON body

4. `res.redirect(url, status)` → `res.redirect(status, url)`

// ❌ v4 - Parámetros en orden incorrecto
res.redirect('/new-url', 301);

// ✅ v5 - Orden correcto (status primero)
res.redirect(301, '/new-url');

// ✅ NestJS way (usando @Redirect decorator)
@Get('old-path')
@Redirect('https://example.com/new-path', 301)
oldPath() {
  return {};
}

Caso Real: Migración de Controller Completo

Migra un controller real de una API de blog:

// ❌ ANTES - Patrones de v4 que causarán errores
@Controller('posts')
export class PostsController {
  constructor(private postsService: PostsService) {}

  @Get()
  findAll(@Res() res: Response) {
    return this.postsService.findAll().then(posts => {
      res.json(posts, 200);  // ❌ Firma deprecada
    });
  }

  @Post()
  create(@Body() createPostDto: CreatePostDto, @Res() res: Response) {
    this.postsService.create(createPostDto).then(post => {
      res.json(post, 201);  // ❌ Firma deprecada
    });
  }

  @Get(':id')
  async findOne(@Param('id') id: string, @Res() res: Response) {
    try {
      const post = await this.postsService.findOne(id);
      res.json(post);  // ✅ Correcto pero no es "NestJS way"
    } catch (error) {
      res.send(404);  // ❌ Ambiguous
    }
  }
}

// ✅ DESPUÉS - Patrones de v5 + NestJS best practices
@Controller('posts')
export class PostsController {
  constructor(private postsService: PostsService) {}

  @Get()
  findAll() {
    // NestJS maneja status 200 automáticamente
    return this.postsService.findAll();
  }

  @Post()
  @HttpCode(201)  // Status explícito con decorator
  create(@Body() createPostDto: CreatePostDto) {
    return this.postsService.create(createPostDto);
  }

  @Get(':id')
  async findOne(@Param('id') id: string) {
    const post = await this.postsService.findOne(id);
    if (!post) {
      // NestJS Exception Filter maneja esto
      throw new NotFoundException(`Post #${id} not found`);
    }
    return post;
  }
}

// Exception filter personalizado para 404
@Catch(NotFoundException)
export class NotFoundFilter implements ExceptionFilter {
  catch(exception: NotFoundException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = HttpStatus.NOT_FOUND;

    response.status(status).json({
      statusCode: status,
      message: exception.message,
      timestamp: new Date().toISOString(),
      path: ctx.getRequest<Request>().url,
    });
  }
}

> 💡 Pro Tip: En NestJS, evita inyectar `@Res()` (Response object) a menos que sea absolutamente necesario. NestJS puede manejar automáticamente la respuesta si devuelves un valor o lanzas una excepción. El uso de `@Res()` desactiva esta funcionalidad.

Breaking Change #4: `req.body` Undefined y Body Parser

Express 5 cambia el comportamiento del body parser de forma subtil pero peligrosa.

El Cambio

// ❌ v4 - req.body siempre era un objeto (vacío si no hay body)
app.post('/user', (req, res) => {
  console.log(req.body);  // {} siempre
});

// ✅ v5 - req.body es undefined si no hay body
app.post('/user', (req, res) => {
  console.log(req.body);  // undefined si no hay body
});

El Problema con Type Guards

Si tu código valida la presencia de propiedades en `req.body` sin verificar primero si existe:

// ❌ PELIGROSO - Fallará en Express 5
@Injectable()
export class ValidationMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    // Esto crashará si req.body es undefined
    if (req.body.email) {
      throw new BadRequestException('Email already exists');
    }
    next();
  }
}

// ✅ SEGURO - Verifica undefined primero
@Injectable()
export class ValidationMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    // Siempre verifica que body existe
    if (req.body && req.body.email) {
      throw new BadRequestException('Email already exists');
    }
    next();
  }
}

Configuración de Body Parser en NestJS 11

NestJS 11 usa por defecto `body-parser` con configuración ajustada para Express 5:

// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Configuración recomendada para Express 5
  app.useBodyParsers('json', {
    limit: '10mb',
    strict: false,  // Permite arrays en root
  });

  app.useBodyParsers('urlencoded', {
    extended: true,  // qs library parsing
    limit: '10mb',
  });

  // ValidationPipe GLOBAL es mejor que middleware manual
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
    transformOptions: {
      enableImplicitConversion: true,
    },
  }));

  await app.listen(3000);
}
bootstrap();

DTO Validation – El Enfoque Correcto

En lugar de validar en middlewares, usa DTOs con class-validator:

// ✅ NESTJS 11 - Enfoque recomendado
import { IsEmail, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsEmail({}, { message: 'Email must be valid' })
  email: string;

  @IsString()
  @MinLength(8, { message: 'Password must be at least 8 characters' })
  password: string;

  @IsString()
  @MinLength(2, { message: 'Name must be at least 2 characters' })
  name: string;
}

// En el controller
@Controller('users')
export class UsersController {
  @Post()
  async create(@Body() createUserDto: CreateUserDto) {
    // ValidationPipe valida automáticamente ANTES de llegar aquí
    // Si req.body es undefined, lanza BadRequestException
    return this.usersService.create(createUserDto);
  }
}

Breaking Change #5: Static Files y Dotfiles

Este cambio puede romper funcionalidades críticas como Apple Universal Links o Android App Links.

El Cambio

En Express 4, los archivos que empezaban con `.` (dotfiles) eran servidos por defecto:

// ❌ v4 - Sirve .well-known por defecto
app.use(express.static('public'));
// GET /.well-known/assetlinks.json → 200 OK (archivo servido)

En Express 5, los dotfiles están bloqueados por seguridad:

// ✅ v5 - No sirve dotfiles por defecto
app.use(express.static('public'));
// GET /.well-known/assetlinks.json → 404 Not Found

La Solución Configurada

// ✅ Configuración segura para dotfiles específicos
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Sirve explícitamente solo dotfiles necesarios
  app.useStaticAssets(
    join(__dirname, '..', 'public'),
    {
      prefix: '/public',
      // Opción 1: Permitir todos (no recomendado)
      // dotfiles: 'allow',

      // Opción 2: Permitir dotfiles específicos (recomendado)
      dotfiles: 'ignore',  // Default
      // Y luego crear rutas específicas:
    }
  );

  // Sirve .well-known explícitamente para App Links
  app.use('/.well-known', express.static(
    join(__dirname, '..', 'public', '.well-known'),
    { dotfiles: 'allow' }
  ));

  await app.listen(3000);
}

Estructura de Archivos Recomendada

project-root/
├── public/
│   ├── .well-known/
│   │   ├── assetlinks.json        # Android App Links
│   │   └── apple-app-site-association  # iOS Universal Links
│   ├── images/
│   └── css/
└── src/

Código de Configuración Completo

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { join } from 'path';
import * as express from 'express';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Configuración de static assets para Express 5
  const staticOptions = {
    // Cache control mejorado
    maxAge: '1d',
    // Etags para cache eficiente
    etag: true,
    // Last-Modified header
    lastModified: true,
    // NO servir dotfiles por defecto (seguridad)
    dotfiles: 'ignore',
  };

  // Static assets principales
  app.useStaticAssets(join(__dirname, '..', 'public'), staticOptions);

  // Dotfiles críticos para deep linking (móvil)
  app.use(
    '/.well-known',
    express.static(
      join(__dirname, '..', 'public', '.well-known'),
      { ...staticOptions, dotfiles: 'allow' }
    )
  );

  await app.listen(3000);
  console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();

Breaking Change #6: Codemods y Herramientas de Migración

Express 5 provee codemods automatizados para ayudar en la migración. NestJS recomienda usarlos antes de actualizar.

Instalación y Uso

# Instala el package de codemods
npm install -g @expressjs/codemod

# Ejecuta TODOS los codemods disponibles
npx @expressjs/codemod upgrade

# O ejecuta codemods específicos
npx @expressjs/codemod v4-deprecated-signatures
npx @expressjs/codemod pluralized-methods
npx @expressjs/codemod req-param
npx @expressjs/codemod magic-redirect

Qué Arreglan los Codemods

| Codemod | Cambios Automáticos |

|———|———————|

| `v4-deprecated-signatures` | `res.json(obj, status)` → `res.status(status).json(obj)` |

| `pluralized-methods` | `req.acceptsCharset()` → `req.acceptsCharsets()` |

| `req-param` | `req.param(‘id’)` → `req.params.id` o `req.body.id` |

| `magic-redirect` | `res.redirect(‘back’)` → `res.redirect(req.get(‘Referrer’) || ‘/’)` |

Integración con NestJS

# Flujo de migración recomendado
npm install @nestjs/cli@latest -g
nest update  # Actualiza NestJS a v11
npx @expressjs/codemod upgrade  # Ejecuta codemods
npm audit fix  # Corrige vulnerabilities
npm test  # Ejecuta tests para detectar problemas

Personalización de Codemods

Si tienes código que no se puede migrar automáticamente:

// Crea un script custom: scripts/migrate-express.js
const { defineTest } = require('@expressjs/codemod');

// Ejemplo: migrar @Get con wildcards sin nombre
module.exports = defineTest({
  name: 'nest-wildcard-routes',
  visitor: {
    Decorator(path) {
      // Lógica para transformar @Get('path/*') a @Get('path/*0')
    },
  },
});

Guía de Migración Paso a Paso: NestJS 10 → 11

Esta es la hoja de ruta completa para migrar sin interrumpir tu producción.

Fase 1: Preparación (1-2 horas)

1.1 Backup y Branching

# Crea branch de migración
git checkout -b feature/nestjs-11-migration

# Tag tu versión actual
git tag -a v10-last-stable -m "Last stable NestJS 10 version"

# Verifica que todos los tests pasan
npm test

1.2 Audit de Dependencias

# Verifica dependencias desactualizadas
npm outdated

# Revisa package.json
cat package.json | grep -A 20 "dependencies"
cat package.json | grep -A 20 "devDependencies"

1.3 Documenta Rutas Actuales

// Crea script: scripts/audit-routes.ts
import { readdirSync } from 'fs';
import { join } from 'path';

function auditControllers(dir: string) {
  const files = readdirSync(dir, { withFileTypes: true });

  for (const file of files) {
    if (file.isDirectory()) {
      auditControllers(join(dir, file.name));
    } else if (file.name.endsWith('.controller.ts')) {
      console.log(`Auditing: ${file.name}`);
      // Extrae rutas con regex
      const content = require(join(dir, file.name)).toString();
      const wildcardRoutes = content.match(/@Get\('[^']*[*][^']*'\)/g);
      if (wildcardRoutes) {
        console.log('  Wildcard routes found:', wildcardRoutes);
      }
    }
  }
}

auditControllers(join(__dirname, '..', 'src'));

Fase 2: Actualización de Dependencias (30 min)

2.1 Actualiza package.json

{
  "dependencies": {
    "@nestjs/common": "^11.0.0",
    "@nestjs/core": "^11.0.0",
    "@nestjs/platform-express": "^11.0.0",
    "express": "^5.0.0",
    "reflect-metadata": "^0.2.1"
  },
  "devDependencies": {
    "@nestjs/cli": "^11.0.0",
    "@nestjs/testing": "^11.0.0",
    "typescript": "^5.6.0"
  }
}
# Limpia node_modules y reinstala
rm -rf node_modules package-lock.json
npm install

2.2 Ejecuta Codemods

# Ejecuta todos los codemods de Express
npx @expressjs/codemod upgrade

# Verifica los cambios
git diff

Fase 3: Migración de Código (2-4 horas)

3.1 Actualiza Wildcards en Controllers

// Encuentra todos los wildcards
//grep -r "@Get.*\*" src/

// Ejemplo de migración
// src/users/users.controller.ts

// ❌ ANTES
@Controller('users')
export class UsersController {
  @Get('files/*')
  getUserFiles() { /* ... */ }

  @Get('posts/*category/:id')
  getPostInCategory() { /* ... */ }
}

// ✅ DESPUÉS
@Controller('users')
export class UsersController {
  @Get('files/*path')
  getUserFiles(@Param('path') path: string) { /* ... */ }

  @Get('posts/*category/:id')
  getPostInCategory(
    @Param('category') category: string,
    @Param('id') id: string
  ) { /* ... */ }
}

3.2 Actualiza Middlewares

// src/middleware/validation.middleware.ts

// ❌ ANTES
@Injectable()
export class ValidationMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    if (req.param('id')) {  // Método deprecado
      // ...
    }
    next();
  }
}

// ✅ DESPUÉS
@Injectable()
export class ValidationMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    // No necesitas try-catch en Express 5
    const id = req.params.id || req.body.id;
    if (!id) {
      throw new BadRequestException('ID is required');
    }
    next();
  }
}

3.3 Actualiza Exception Filters

// src/filters/all-exceptions.filter.ts

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    // Express 5: req.body puede ser undefined
    const body = request.body || {};

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      body: process.env.NODE_ENV === 'development' ? body : undefined,
      message:
        exception instanceof HttpException
          ? exception.message
          : 'Internal server error',
    });
  }
}

Fase 4: Configuración (30 min)

4.1 Actualiza main.ts

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe, Logger } from '@nestjs/common';
import { AppModule } from './app.module';

// ✅ NestJS 11: JSON Logger nativo
async function bootstrap() {
  const logger = new Logger('Bootstrap');

  const app = await NestFactory.create(AppModule, {
    logger: new Logger({
      json: true,  // Formato JSON para producción
      colors: process.env.NODE_ENV !== 'production',
      timestamp: true,
    }),
  });

  // ValidationPipe con opciones mejoradas
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
    transformOptions: {
      enableImplicitConversion: true,
    },
    disableErrorMessages: process.env.NODE_ENV === 'production',
  }));

  // CORS
  app.enableCors({
    origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
    credentials: true,
  });

  const port = process.env.PORT || 3000;
  await app.listen(port);

  logger.log(`🚀 Application running on port ${port}`);
  logger.log(`📊 Environment: ${process.env.NODE_ENV}`);
}

bootstrap();

Fase 5: Testing y Validación (1-2 horas)

5.1 Suite de Tests de Migración

// test/migration.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('Migration to NestJS 11', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  describe('Wildcard Routes (Express 5 syntax)', () => {
    it('/api/files/*path should match nested paths', () => {
      return request(app.getHttpServer())
        .get('/api/files/docs/report.pdf')
        .expect(200)
        .expect((res) => {
          expect(res.body.path).toBe('docs/report.pdf');
        });
    });

    it('/api/files/{*path} should match root path too', () => {
      return request(app.getHttpServer())
        .get('/api/files')
        .expect(200);
    });
  });

  describe('Optional Parameters (Express 5 syntax)', () => {
    it('/api/assets/:file{.:ext} should work without extension', () => {
      return request(app.getHttpServer())
        .get('/api/assets/logo')
        .expect(200)
        .expect((res) => {
          expect(res.body.file).toBe('logo');
          expect(res.body.ext).toBeUndefined();
        });
    });

    it('/api/assets/:file{.:ext} should work with extension', () => {
      return request(app.getHttpServer())
        .get('/api/assets/logo.png')
        .expect(200)
        .expect((res) => {
          expect(res.body.file).toBe('logo');
          expect(res.body.ext).toBe('png');
        });
    });
  });

  describe('Error Handling (Async errors)', () => {
    it('should catch async errors in middlewares', () => {
      return request(app.getHttpServer())
        .get('/api/protected')
        .expect(401);
    });
  });

  describe('Body Parsing (req.body undefined)', () => {
    it('should handle empty body correctly', () => {
      return request(app.getHttpServer())
        .post('/api/users')
        .send()
        .expect(400)
        .expect((res) => {
          expect(res.body.message).toContain('body');
        });
    });
  });
});

5.2 Script de Verificación

#!/bin/bash
# scripts/verify-migration.sh

echo "🔍 Verificando migración a NestJS 11..."

# 1. Verifica versión de NestJS
echo "📦 Verificando versiones..."
nest --version
node -v

# 2. Ejecuta tests
echo "🧪 Ejecutando suite de tests..."
npm test

# 3. Verifica linting
echo "🔍 Verificando linting..."
npm run lint

# 4. Build verification
echo "🏗️ Verificando build..."
npm run build

# 5. E2E tests
echo "🚀 Ejecutando E2E tests..."
npm run test:e2e

echo "✅ Verificación completa!"

Fase 6: Deployment (30 min)

6.1 Checklist Pre-Production

// Crea: checklists/pre-deployment.md
# ✅ Checklist Pre-Deployment NestJS 11

## Dependencias
- [ ] Todas las dependencias actualizadas a v11
- [ ] No hay vulnerabilidades (npm audit)
- [ ] Codemods ejecutados correctamente

## Código
- [ ] Wildcards convertidos a sintaxis de Express 5
- [ ] Middlewares async sin try-catch innecesario
- [ ] req.body verificado undefined antes de uso
- [ ] Dotfiles configurados explícitamente

## Configuración
- [ ] main.ts actualizado con JSON logger
- [ ] ValidationPipe global configurado
- [ ] Body parser configurado correctamente

## Testing
- [ ] Unit tests pasan (100%)
- [ ] E2E tests pasan (100%)
- [ ] Tests de carga ejecutados sin regresiones

## Documentación
- [ ] README actualizado con requisitos
- [ ] Changelog documentado
- [ ] Equipo notificado de breaking changes

6.2 Deployment con Feature Flags

// src/config/feature-flags.ts
export const featureFlags = {
  // Activa gradualmente nuevas características
  useExpress5: process.env.USE_EXPRESS_5 === 'true',
  useJsonLogger: process.env.USE_JSON_LOGGER === 'true',
};

// main.ts
if (featureFlags.useJsonLogger) {
  app.useLogger(new Logger({ json: true }));
}

Problemas Comunes y Soluciones

Estos son los problemas más frecuentes reportados por la comunidad durante la migración.

Problema #1: Tests Unitarios Fallando

Síntoma: Tests unitarios de controllers fallan con “Cannot read property ‘params’ of undefined”.

Causa: Los mocks de Request/Response no están actualizados para Express 5.

Solución:

// ❌ ANTES - Mock incompleto
const mockRequest = {};
const mockResponse = {};

// ✅ DESPUÉS - Mock completo para Express 5
const mockRequest = {
  params: {},
  query: {},
  body: undefined,  // Express 5: undefined si no hay body
  headers: {},
  url: '/test',
  method: 'GET',
};

const mockResponse = {
  status: jest.fn().mockReturnThis(),
  json: jest.fn().mockReturnThis(),
  send: jest.fn().mockReturnThis(),
};

// En el test
describe('UsersController', () => {
  let controller: UsersController;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [UsersController],
      providers: [UsersService],
    }).compile();

    controller = module.get<UsersController>(UsersController);
  });

  it('should handle empty request body', () => {
    // Mock explícito de body como undefined
    const mockReq = { ...mockRequest, body: undefined };

    expect(() => controller.create(mockReq)).not.toThrow();
  });
});

Problema #2: Guards/Interceptors No Ejecutándose

Síntoma: Guards o interceptors no se ejecutan después de migrar.

Causa: El uso de `@Res()` desactiva los post-interceptors de NestJS.

Solución:

// ❌ INCORRECTO - @Res() desactiva interceptors
@Controller('users')
export class UsersController {
  @Get()
  findAll(@Res() res: Response) {
    return res.json([]);
    // Interceptors no se ejecutarán
  }
}

// ✅ CORRECTO - Deja que NestJS maneje la respuesta
@Controller('users')
export class UsersController {
  @Get()
  findAll() {
    return [];  // NestJS llama res.json() automáticamente
    // Interceptors SÍ se ejecutan
  }
}

// ✅ Si REALMENTE necesitas @Res() (raro)
@Controller('users')
export class UsersController {
  @Get()
  @HttpCode(HttpStatus.OK)  // Debe ser explícito
  findAll(@Res({ passthrough: true }) res: Response) {
    // passthrough: true permite que interceptors funcionen
    res.setHeader('X-Custom-Header', 'value');
    return [];  // NestJS aún maneja el return
  }
}

Problema #3: WebSockets/Socket.io No Funcionan

Síntoma: Conexiones WebSocket rechazadas después de migrar.

Causa: El orden de inicialización de gateways cambió ligeramente.

Solución:

// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // ✅ CORRECTO: Inicializa gateways ANTES de listen
  const gateway = app.getGateway(MyGateway);

  await app.listen(3000);
}

// gateway/my.gateway.ts
@WebSocketGateway({
  cors: {
    origin: process.env.CLIENT_URL,
    credentials: true,
  },
  transports: ['websocket', 'polling'],
})
export class MyGateway implements OnGatewayInit {
  afterInit(server: Server) {
    this.logger.log('WebSocket server initialized');
  }
}

Problema #4: Performance Degradation

Síntoma: API responde más lento después de migrar.

Causa Probable: JSON logging sin colors en desarrollo.

Solución:

// main.ts
const isDevelopment = process.env.NODE_ENV !== 'production';

const app = await NestFactory.create(AppModule, {
  logger: new Logger({
    json: !isDevelopment,  // Solo JSON en producción
    colors: isDevelopment,  // Colors en desarrollo
    timestamp: true,
  }),
});

Problema #5: Módulo Circular Dependencies

Síntoma: Error “Circular dependency detected” después de actualizar.

Causa: El nuevo sistema de opaque keys en módulos dinámicos.

Solución:

// ❌ ANTES - Misma configuración en múltiples imports
@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    ConfigModule.forRoot({ isGlobal: true }),  // Duplicado
  ],
})
export class AppModule {}

// ✅ DESPUÉS - Reusa la instancia
const configModule = ConfigModule.forRoot({ isGlobal: true });

@Module({
  imports: [configModule, configModule],  // Misma instancia
})
export class AppModule {}

Optimización de Rendimiento en NestJS 11

Aprovecha las nuevas optimizaciones para maximizar el rendimiento.

1. JSON Logging Nativo

El nuevo ConsoleLogger con JSON es 40% más rápido que bibliotecas third-party como winston o pino:

// main.ts
import { Logger } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: new Logger({
      json: true,
      colors: false,
      timestamp: true,
      // Formato personalizado optimizado
      formatter: (logLevel, message, meta) => ({
        level: logLevel,
        message,
        ...meta,
        timestamp: new Date().toISOString(),
        service: 'api-gateway',
        environment: process.env.NODE_ENV,
      }),
    }),
  });
}

2. Startup Time Mejorado

NestJS 11 optimizó la generación de opaque keys para módulos dinámicos:

// ✅ Para módulos dinámicos pesados
@Module({
  imports: [
    // Usa referencias en lugar de recrear
    DynamicModule.forRoot(config),
  ],
})
export class AppModule {}

// Benchmark:
// NestJS 10: startup 3.2s (con 50 módulos dinámicos)
// NestJS 11: startup 1.9s (40% más rápido)

3. Lazy Loading de Módulos

// ✅ Carga módulos solo cuando se necesitan
@Module({
  imports: [
    // Lazy loading de rutas administrativas
    {
      path: 'admin',
      module: AdminModule,
      loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
    },
  ],
})
export class AppModule {}

Buenas Prácticas Post-Migración

Una vez migrado, sigue estos patrones recomendados para mantener tu código limpio y mantenible.

1. Usa Type-Safe Route Parameters

// ✅ Define tipos específicos para parámetros
interface RouteParams {
  userId: string;
  postId?: string;
}

@Controller('users')
export class UsersController {
  @Get(':userId/posts/:postId?')
  getUserPosts(
    @Param() params: RouteParams
  ) {
    // params.userId y params.postId están tipados
    return this.postsService.findByUser(params.userId, params.postId);
  }
}

2. Implementa Request Validation Middleware

// middleware/validation.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class RequestValidationMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    // Express 5: req.body puede ser undefined
    if (req.method === 'POST' || req.method === 'PATCH') {
      if (req.body === undefined || req.body === null) {
        throw new BadRequestException('Request body is required');
      }

      // Verifica Content-Type header
      const contentType = req.headers['content-type'];
      if (
        contentType?.includes('application/json') &&
        typeof req.body !== 'object'
      ) {
        throw new BadRequestException('Invalid JSON body');
      }
    }

    next();
  }
}

// En el módulo
@Module({
  providers: [RequestValidationMiddleware],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(RequestValidationMiddleware)
      .forRoutes('*');  // Aplica a todas las rutas
  }
}

3. Health Check Endpoint

// health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import {
  HealthCheck,
  HealthCheckService,
  MemoryHealthIndicator,
  DiskHealthIndicator,
} from '@nestjs/terminus';

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private memory: MemoryHealthIndicator,
    private disk: DiskHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      // El heap debe usar menos de 250MB
      () => this.memory.checkHeap('memory_heap', 250 * 1024 * 1024),

      // El heap RSS debe usar menos de 300MB
      () => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024),

      // El disco debe tener menos del 75% usado
      () => this.disk.checkStorage('storage', {
        path: '/',
        thresholdPercent: 0.75,
      }),
    ]);
  }
}

4. Structured Error Logging

// filters/all-exceptions.filter.ts
import { Catch, ExceptionFilter, HttpException } from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    // Estructura de error consistente
    const errorResponse = {
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      // Información adicional solo en desarrollo
      ...(process.env.NODE_ENV === 'development' && {
        stack: exception instanceof Error ? exception.stack : undefined,
        query: request.query,
        body: request.body,
      }),
    };

    // Log estructurado
    console.error(JSON.stringify({
      level: 'error',
      ...errorResponse,
      message: exception instanceof Error ? exception.message : 'Unknown error',
    }));

    response.status(status).json(errorResponse);
  }
}

Preguntas Frecuentes (FAQ)

1. ¿Debo migrar a Express 5 o puedo quedarme en Express 4 con NestJS 11?

Puedes quedarte en Express 4, pero no es recomendable. NestJS 11 está optimizado para Express 5, y quedarte en v4 significa perder mejoras de rendimiento y características nuevas. Para usar Express 4, debes configurarlo explícitamente en `main.ts`:

import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import * as express from 'express';

const server = express();  // Express 4 instance

const app = await NestFactory.create(
  AppModule,
  new ExpressAdapter(server)
);

Sin embargo, Express 4 está en maintenance mode y no recibirá updates de seguridad a largo plazo. La migración a Express 5 es inevitable y mejor hacerla ahora.

2. ¿Qué pasa con mis middlewares personalizados? ¿Necesito reescribirlos todos?

No necesariamente. Los middlewares siguen funcionando, pero hay dos cambios a considerar:

1. Error handling asíncrono mejorado: Ya no necesitas `try-catch` para capturar promesas rechazadas; Express 5 lo hace automáticamente.

2. req.body undefined: Si tu middleware asume que `req.body` siempre es un objeto, debes agregar validaciones.

// Middleware que NO necesita cambios
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    console.log(`${req.method} ${req.url}`);
    next();
  }
}

// Middleware que NECESITA cambios
@Injectable()
export class BodyParserMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    // ✅ Agrega verificación de undefined
    if (req.body === undefined) {
      req.body = {};  // O lanza error
    }
    next();
  }
}

3. ¿Los tests unitarios de mis controllers seguirán funcionando?

Depende de cómo estén escritos. Si usas NestJS Testing Utilities (`Test.createTestingModule`), todo debería funcionar. Si haces mocks manuales de Request/Response, necesitas actualizarlos:

// ❌ Mock que fallará
const mockRequest = { params: { id: '123' } };

// ✅ Mock correcto para Express 5
const mockRequest = {
  params: { id: '123' },
  query: {},
  body: undefined,  // Express 5 puede ser undefined
  headers: {},
  url: '/users/123',
  method: 'GET',
};

4. ¿Cómo manejo rutas legacy que usaban regexp characters como `[abc]`?

Express 5 no soporta regexp characters en path strings. Debes convertirlas a arrays de rutas:

// ❌ Express 4 - Regexp en path
@Get('[users|admins]/:id')
findOne(@Param('id') id: string) {
  return this.usersService.findOne(id);
}

// ✅ Express 5 - Array de rutas
@Get(['users/:id', 'admins/:id'])
findOne(@Param('id') id: string) {
  return this.usersService.findOne(id);
}

Si necesitas lógica más compleja, usa Guards para filtrar:

@Controller()
export class UsersController {
  @Get(':type/:id')
  findOne(
    @Param('type') type: string,
    @Param('id') id: string,
  ) {
    if (!['users', 'admins'].includes(type)) {
      throw new NotFoundException('Invalid type');
    }
    return this.usersService.findOne(type, id);
  }
}

5. ¿Es seguro migrar en producción sin downtime?

Sí, si usas deployment strategies como Blue-Green o Canary:

# Estrategia Blue-Green
# 1. Despliega NestJS 11 en entorno de staging
# 2. Ejecuta smoke tests completos
# 3. Cambia tráfico del load balancer gradualmente:
#    - 10% → NestJS 11
#    - 50% → NestJS 11
#    - 100% → NestJS 11
# 4. Si hay errores, rollback instantáneo a v10

NestJS 11 es backward compatible en la mayoría de casos, excepto por las rutas con wildcards malformadas.

6. ¿Necesito actualizar mis DTOs con class-validator?

No es estrictamente necesario, pero es altamente recomendado. Express 5 introduce cambios sutiles en cómo se parsea el body, y class-validator actúa como una capa de seguridad adicional:

import { IsString, IsEmail, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;
}

// En main.ts
app.useGlobalPipes(new ValidationPipe({
  transform: true,
  whitelist: true,
  forbidNonWhitelisted: true,
}));

7. ¿Cómo pruebo la migración sin afectar mi base de datos real?

Usa Docker containers para una base de datos de prueba:

# docker-compose.test.yml
version: '3.8'
services:
  postgres-test:
    image: postgres:16
    environment:
      POSTGRES_DB: test_db
      POSTGRES_USER: test_user
      POSTGRES_PASSWORD: test_pass
    ports:
      - "5433:5432"
  app-test:
    build: .
    environment:
      DATABASE_URL: postgresql://test_user:test_pass@postgres-test:5432/test_db
    command: npm run test:e2e
// test/setup.e2e.ts
beforeAll(async () => {
  // Usa base de datos de prueba
  process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;

  const moduleFixture: TestingModule = await Test.createTestingModule({
    imports: [AppModule],
  }).compile();

  app = moduleFixture.createNestApplication();
  await app.init();
});

Takeaways Clave

– 🎯 Express 5 es el default en NestJS 11: La migración es inevitable pero trae mejoras de 15-40% en rendimiento.

– 🎯 Wildcard routing cambió radicalmente: `/` ahora requiere nombre (`/splat`), `?` fue reemplazado por llaves (`{.:ext}`), y regexp characters no son soportados.

– 🎯 Error handling asíncrono es automático: Ya no necesitas `try-catch` en middlewares async; Express 5 captura promesas rechazadas.

– 🎯 req.body puede ser undefined: Express 5 devuelve `undefined` en lugar de `{}` cuando no hay body, requiriendo validaciones adicionales.

– 🎯 Codemods aceleran la migración: Usa `npx @expressjs/codemod upgrade` para convertir automáticamente patrones deprecados.

– 🎯 JSON logging nativo: El nuevo ConsoleLogger es más rápido que winston/pino y está integrado en el framework.

– 🎯 Startup time reducido 40%: Para aplicaciones con muchos módulos dinámicos, gracias a la optimización en opaque key generation.

– 🎯 Deployment gradual es posible: Usa feature flags y blue-green deployment para migrar sin downtime.

Conclusión

La migración a NestJS 11 con Express 5 no es simplemente una actualización de dependencias: representa un salto arquitectónico significativo que moderniza tu stack para los próximos años. Aunque los breaking changes pueden parecer intimidantes inicialmente, las mejoras de rendimiento (15-40% más rápido), mejor manejo de errores y herramientas de migración automatizadas hacen que el esfuerzo valga la pena.

Hemos cubierto los cambios más impactantes: sintaxis de wildcards, error handling asíncrono, métodos deprecados, body parsing, y static files. Con los 10 ejemplos de código y la guía paso a paso, tienes un roadmap claro para ejecutar la migración sin interrumpir tu producción.

> El futuro de NestJS es Express 5. La comunidad ya está migrando masivamente, y quedarse en Express 4 significa perder years de optimizaciones y patches de seguridad.

Próximos pasos inmediatos:

1. Crea un branch `feature/nestjs-11-migration`

2. Ejecuta `npx @expressjs/codemod upgrade`

3. Actualiza todas las rutas con wildcards

4. Ejecuta la suite de tests completa

5. Deploy en staging y valida con smoke tests

6. Lanza a producción usando blue-green deployment

La comunidad de NestJS ha documentado extensivamente esta migración. Si encuentras problemas edge case, revisa los issues oficiales en GitHub o los foros de la comunidad. La inversión de tiempo ahora se pagará con una API más rápida, mantenible y futura-proof.

Recursos Adicionales

Documentación Oficial

NestJS 11 Migration Guide – Guía oficial completa de migración

Express 5 Migration Guide – Detalle de todos los breaking changes

Trilon – Announcing NestJS 11 – Anuncio oficial con mejoras detalladas

NestJS Documentation – Documentación actualizada v11

Herramientas

@expressjs/codemod – Codemods oficiales para migración automática

@nestjs/cli – CLI para creación y migración de proyectos

TypeScript 5.6 – Tipado mejorado

Artículos de Profundización

What’s New in Express.js v5.0 – Guía comprehensiva de Express 5

Express.js 5 Migration Guide (LogRocket) – Tutorial con ejemplos prácticos

An Introductory Guide to Migrating Express.js V4 to V5 – Guía para principiantes

Repositorios de Ejemplo

NestJS 11 Example Repository

Express 5 Breaking Changes Examples

Comunidad

NestJS Discord

NestJS GitHub Discussions

Stack Overflow – NestJS Tag

Ruta de Aprendizaje (Siguientes Pasos)

Ahora que dominas la migración a NestJS 11, continúa tu aprendizaje con estos temas avanzados:

1. Microservicios con NestJS 11:

Las nuevas características de `ClientProxy` (`unwrap`, `on`, `status`) permiten un control sin precedentes sobre conexiones a message brokers como NATS, Kafka y Redis. Aprende a implementar arquitecturas event-driven reactivas con observabilidad nativa.

2. Testing Avanzado con NestJS 11:

Profundiza en estrategias de testing para aplicaciones NestJS modernas: integrales tests de contratos (contract testing), load testing con K6, y testing de microservicios con containers Docker aislados.

3. Performance Optimization y Caching:

El nuevo `CacheModule` con `cache-manager` v6 y `Keyv` ofrece una interfaz unificada para múltiples backends (Redis, Memcached, etcd). Aprende patrones de caching avanzados como cache stampede prevention y stale-while-revalidate.

Challenge Práctico

Objetivo: Migrar una API real de NestJS 10 a NestJS 11 implementando todos los cambios aprendidos.

Requisitos Mínimos:

1. Crear API en NestJS 10 (si no tienes una):

– Controller `UsersController` con endpoints CRUD

– Al menos 3 rutas con wildcards: `/users/`, `/users/:id{.:format}`, `/users/:userId/posts/category`

– Middleware de validación async personalizado

– Exception filter global

– DTOs con class-validator

2. Migrar a NestJS 11:

– Actualizar `package.json` a v11

– Ejecutar `npx @expressjs/codemod upgrade`

– Convertir todas las rutas con wildcards a sintaxis Express 5

– Remover `try-catch` innecesarios en middlewares async

– Configurar JSON logger nativo

– Configurar serving de archivos estáticos con dotfiles explícitos

3. Validar con Tests:

– Suite de unit tests (mínimo 80% coverage)

– Tests E2E para todas las rutas con wildcards

– Tests de error handling asíncrono

– Tests de body parsing con `req.body` undefined

BONUS (Avanzado):

– Implementar feature flags para migración gradual

– Crear script de verificación automática (como el provided)

– Hacer deployment a staging environment con Docker

– Ejecutar load tests antes/después para comparar performance

Tiempo Estimado: 2-3 horas

Criterios de Éxito:

– [ ] Todos los tests pasan sin errores

– [ ] No hay warnings de deprecation

– [ ] API responde en igual o menor tiempo que antes

– [ ] Logs estructurados en JSON

– [ ] Código sigue las mejores prácticas de NestJS 11

Recursos de Apoyo:

– Usa los ejemplos de código de este artículo como templates

– Consulta los recursos oficiales cuando bloquees

– Comparte tu progreso en issues de GitHub o foros de la comunidad

¡El challenge comienza ahora! 🚀

Firma del Artículo:

Este artículo fue creado para desarrolladores que buscan dominar la migración a NestJS 11 y Express 5. Basado en documentación oficial, investigación técnica y experiencias reales de migración en producción. Última actualización: Enero 2026.

¿Encontraste útil este artículo? Compártelo con tu equipo y ayudemos a la comunidad a migrar exitosamente a NestJS 11. 🐈

Deja un comentario

Scroll al inicio

Discover more from Creapolis

Subscribe now to keep reading and get access to the full archive.

Continue reading