Parte VIII · Ampliaciones

31. Actualizar y migrar proyectos heredados

La mayoría de los equipos no empiezan TaskFlow desde cero: heredan un monolito Angular 12 + NestJS 8 + MikroORM 5 que «funciona en producción» y que nadie se atreve a tocar. Este capítulo enseña a inventariar, planificar y ejecutar actualizaciones y migraciones con riesgo controlado: de NgModules a standalone, de Zone.js a zoneless, de HttpModule a interceptores funcionales, de MikroORM 5 a 6, y de esquemas de base de datos con downtime a despliegues expand/contract. El objetivo no es la versión más nueva por snobismo: es recuperar velocidad de entrega sin apostar la estabilidad del negocio.

AVANZADO Tiempo de lectura: ~95 min Prerrequisitos: capítulos 2, 8, 9, 13 y 21

31.1 Qué vas a poder hacer al terminar

  • Diagnosticar la deuda técnica de un TaskFlow heredado (dependencias, Node LTS, TypeScript strict, tests y pipelines) y convertirla en un inventario accionable.
  • Elegir con criterio entre migración big bang, incremental, strangler fig y feature flags, justificando el riesgo residual ante producto y operaciones.
  • Actualizar Angular con ng update y update.angular.dev, anticipando breaking changes típicos (modules→standalone, control flow, signals).
  • Actualizar NestJS (Express/Fastify, peer dependencies, breaking changes) y MikroORM 5→6 con migraciones de esquema coherentes.
  • Migrar NgModules a standalone, Zone.js a zoneless/signals, e interceptores/HttpModule antiguos a la API moderna, con codemods y reglas ESLint que eviten regresiones.
  • Diseñar migraciones de base de datos zero-downtime con el patrón expand/contract, y un plan de rollback y canary listo antes del release.
  • Completar un checklist de release post-migración y reconocer los errores más caros que cometen los equipos al actualizar «porque toca».
Hilo conductor: TaskFlow Legacy

A lo largo del capítulo trabajamos con un escenario realista: TaskFlow 2022, un producto de gestión de tareas con Angular 12 (NgModules, ViewEngine ya en Ivy, HttpClientModule clásico), NestJS 8 (Express, decoradores antiguos), MikroORM 5 y PostgreSQL 13. El equipo quiere llegar a Angular actual, NestJS actual y MikroORM 6 sin un «fin de semana heroico» que deje la aplicación tirada el lunes. Cada sección muestra inventarios, comandos, diffs y decisiones de arquitectura sobre ese mismo producto.

31.2 El problema: deuda técnica, miedo a actualizar, «si funciona no lo toques»

La frase «si funciona, no lo toques» es el mayor generador de coste oculto en equipos de producto. Un TaskFlow que lleva tres años sin actualizar no es un sistema estable: es un sistema congelado. Cada mes que pasa, la distancia con el ecosistema crece, los parches de seguridad dejan de llegar, las bibliotecas de terceros abandonan tu rama mayor y el próximo intento de actualizar cuesta el doble.

La deuda técnica no es solo código feo. En migraciones de stack full-stack aparece en cuatro capas:

Analogía: el edificio y la normativa sísmica

Actualizar un framework es como reforzar un edificio antiguo ante nueva normativa sísmica. Puedes ignorar la normativa mientras no haya terremoto; el día que llegue, el coste de la improvisación supera con creces el de las intervenciones planificadas. La migración incremental es apuntalar planta a planta sin desalojar a los inquilinos; el big bang es derruir y reconstruir en un fin de semana. Ambas son válidas, pero solo una es honestamente aplicable a un producto con usuarios reales.

31.2.1 Por qué los equipos tienen miedo (y cuándo ese miedo es racional)

El miedo no es irracional. Actualizar Angular de 12 a 19 de un golpe, sin tests de contrato ni entorno de preproducción fiel, es una receta para un incidente. Lo irracional es convertir ese miedo en política permanente. Un miedo racional se traduce en: inventario, estrategia, flags, canary y rollback. Un miedo irracional se traduce en: «esperamos a reescribirlo todo el año que viene».

En TaskFlow Legacy, el equipo identificó tres síntomas clásicos:

  1. El npm audit muestra vulnerabilidades críticas en dependencias transitivas que ya no reciben parches en la major actual.
  2. Un desarrollador nuevo tarda dos días en arrancar el entorno porque Node 16 ya no se instala limpio en macOS reciente y el Dockerfile usa una imagen retirada.
  3. Una librería de UI esencial abandona Angular 12; el equipo empieza a forkearla «temporalmente».
Señal de alarma

Cuando el plan de migración se convierte en «reescribir TaskFlow en el framework de moda», has dejado de migrar y has empezado a especular. Las reescrituras totales fracasan con una frecuencia documentada (ver el ensayo clásico de Joel Spolsky sobre Netscape). Este capítulo asume que conservas el valor de negocio y actualizas el andamiaje técnico.

31.2.2 El coste compuesto de no actualizar

Cada major saltada no suma linealmente: multiplica. Angular publica guías de actualización por saltos de una o pocas majors; saltar siete de golpe obliga a encadenar breaking changes, codemods y cambios de mentalidad (RxJS operators pipeables ya asumidos, Ivy obligatorio, standalone por defecto, control flow nuevo). NestJS y MikroORM muestran el mismo patrón. Por eso la estrategia correcta a medio plazo es actualizar de forma continua (al menos dentro de la ventana LTS), no acumular y luego «hacer el proyecto de migración del Q3».

  Coste de actualizar (cualitativo)

  ^
  │                                    ╭──── big bang acumulado
  │                               ╭────╯
  │                          ╭────╯
  │                     ╭────╯
  │                ╭────╯
  │           ╭────╯
  │      ╭────╯  ← actualizaciones frecuentes (pequeñas)
  │ ─────╯
  └──────────────────────────────────────────► tiempo / majors saltadas

31.3 Inventario: dependencias, Node LTS, versiones y TypeScript strict

Ninguna migración seria empieza con ng update. Empieza con un inventario que responda a cinco preguntas: ¿dónde estamos?, ¿qué nos obliga el negocio?, ¿qué nos bloquea?, ¿qué tests nos protegen?, ¿cómo medimos el éxito? Sin ese mapa, cada PR de actualización es un acto de fe.

31.3.1 Inventario de dependencias y runtime

Para TaskFlow Legacy, el inventario mínimo cabe en una tabla viva (Confluence, Notion o un MIGRATION.md en el repo). Lo importante es que sea verificable con comandos, no con recuerdos.

scripts/inventory.sh
#!/usr/bin/env bash
set -euo pipefail

echo "=== Node / npm ==="
node -v
npm -v

echo "=== Angular ==="
npx ng version || true

echo "=== Nest / MikroORM (desde package.json) ==="
node -e "const p=require('./package.json'); \
  console.log('nest', p.dependencies['@nestjs/core']); \
  console.log('mikro', p.dependencies['@mikro-orm/core']); \
  console.log('typescript', p.devDependencies.typescript);"

echo "=== Auditoría ==="
npm audit --json > audit-report.json || true

echo "=== Outdated ==="
npm outdated || true
ÁreaTaskFlow Legacy (ejemplo)Objetivo razonableRiesgo si se ignora
Node16.20LTS activa (p. ej. 20 o 22)Imágenes CI rotas, paquetes sin binarios
Angular12.2Major actual o LTS del equipoSin seguridad, sin standalone/signals
NestJS8.xMajor actual alineada con NodePeer deps rotos, Express tipado antiguo
MikroORM5.x6.xAPI de EntityManager/migraciones distinta
TypeScript4.3, strict parcial5.x con strict: trueRegresiones silenciosas al tipar más
RxJS6.x7.xOperadores y toPromise deprecados
PostgreSQL1315/16 según opsFeatures y parches; planificar con DBA
Node LTS primero

Actualizar Angular o Nest sin mover Node a una LTS soportada es construir sobre arena. Comprueba la matriz oficial de cada framework: Nest documenta versiones de Node soportadas por major; Angular CLI también. Si el Dockerfile fija node:16-alpine, ese cambio es un prerrequisito, no un detalle.

31.3.2 TypeScript strict como red de seguridad

Migrar con strict: false es volar sin instrumentos. Antes de tocar majors de frameworks, activa (aunque sea de forma gradual con strictNullChecks primero) el modo estricto y corrige los errores que ya existían. Cada any que «arregla» un fallo de compilación tras un update es una bomba de relojería en producción.

tsconfig.jsonINCORRECTO
{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": false,
    "skipLibCheck": true,
    "suppressImplicitAnyIndexErrors": true
  }
}
tsconfig.jsonCORRECTO
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": false,
    "skipLibCheck": true
  }
}

skipLibCheck: true sigue siendo pragmático en monorepos grandes (evita pelearte con declaraciones de terceros inconsistentes entre sí). Lo que no es pragmático es desactivar strict «para que compile el update».

31.3.3 Inventario de tests y contratos

Sin una red de tests mínima, la migración es teatro. El capítulo 8 (Angular testing) y el 13 (Nest testing) son prerrequisitos porque aquí los usamos como criterio de salida de cada fase:

docs/MIGRATION_INVENTORY.md
# Extracto del inventario TaskFlow (markdown en repo)

## Runtime
- Node: 16.20.2 (CI + prod)
- npm: 8.19

## Apps
- apps/web: Angular 12.2.13, RxJS 6.6, Zone.js 0.11
- apps/api: NestJS 8.4, Express 4.18, MikroORM 5.6, PG 13

## Cobertura crítica
- Unit web: 42% (flaky en TaskListComponent)
- Unit api: 61%
- E2E smoke: 6 escenarios Playwright (solo staging)
- Contract: OpenAPI generado, sin validación en CI

## Bloqueadores conocidos
- librería UI X solo soporta Angular <=13
- decoradores Reflect metadata con emitDecoratorMetadata=true obligatorio
- script de deploy asume dist/browser layout Angular 12

31.4 Estrategia de migración: big bang vs incremental, strangler fig, feature flags

La estrategia responde a una pregunta de gestión de riesgo: ¿cuánto cambio quieres que cruce el límite de producción en un solo despliegue? No hay una respuesta universal; hay una respuesta honestamente alineada con tu cobertura de tests, tu capacidad de rollback y la tolerancia al fallo del negocio.

31.4.1 Big bang

Un único tren: actualizas Node, Angular, Nest, MikroORM, esquema y front en una rama larga y despliegas todo junto. Tiene sentido solo cuando el producto es pequeño, el equipo cabe en una pizza, hay ventana de mantenimiento aceptada y la suite de tests es excelente. En TaskFlow con clientes B2B y SLA, el big bang puro suele ser imprudente.

Big bang disfrazado

Una rama migration/angular-19 abierta trece semanas con 400 archivos tocados es un big bang, aunque digas que es incremental. Si no puedes fusionar a main cada pocos días detrás de flags, estás acumulando riesgo de integración.

31.4.2 Incremental por capas

Orden recomendado para TaskFlow (de menor a mayor acoplamiento visible al usuario):

  1. Node LTS + herramientas de build (CLI, webpack/esbuild/vite según versión).
  2. TypeScript strict y limpieza de any críticos.
  3. NestJS + MikroORM en la API (contratos HTTP estables).
  4. Angular major a major (o siguiendo el generador oficial de saltos).
  5. Refactors estructurales: standalone, control flow, signals, zoneless.
  6. Esquema DB con expand/contract desacoplado del deploy de código cuando haga falta.
  Estrategia incremental TaskFlow
  ================================

  semana 1-2   [Node 20 + CI + Docker]
       │
       ▼
  semana 3-4   [Nest 9→10 + peers]──── API canary 10%
       │
       ▼
  semana 5-6   [MikroORM 5→6 + migraciones expand]
       │
       ▼
  semana 7-10  [Angular 12→15→17→actual]  (PRs pequeños)
       │
       ▼
  semana 11+   [standalone / signals / zoneless]
               detrás de feature flags por ruta

31.4.3 Strangler fig

El patrón strangler fig (Martin Fowler) consiste en rodear el sistema legado con uno nuevo que va «estrangulando» rutas o capacidades hasta que el legado puede apagarse. En front, puede significar cargar microfrontends o rutas lazy nuevas en Angular moderno mientras el shell antiguo sigue vivo. En back, puede significar un BFF Nest nuevo que proxya al monolito Nest 8 hasta reimplementar módulos.

Para TaskFlow, un strangler pragmático es: nuevas features solo en módulos ya migrados a standalone + Nest actual; el módulo de informes legacy se toca lo mínimo hasta su turno. No hace falta Kubernetes ni service mesh para aplicar la idea: basta enrutado y disciplina de equipo.

apps/web/src/app/app.routes.ts
import { Routes } from '@angular/router';
import { legacyGuard } from './migration/legacy.guard';

export const APP_ROUTES: Routes = [
  {
    path: 'tasks',
    // Ya migrado: standalone + signals
    loadChildren: () => import('./tasks/tasks.routes').then((m) => m.TASK_ROUTES),
  },
  {
    path: 'reports',
    // Legado: aún NgModule; se estrangula después
    canMatch: [legacyGuard],
    loadChildren: () =>
      import('./reports/reports.module').then((m) => m.ReportsModule),
  },
];

31.4.4 Feature flags

Las feature flags desacoplan desplegar de activar. El código nuevo puede estar en producción apagado; se enciende por porcentaje de usuarios, por tenant o por lista interna. En migraciones de UI (nuevo control flow, nueva pantalla de tareas) son casi obligatorias. En cambios de esquema DB no sustituyen expand/contract, pero ayudan a conmutar lecturas entre columnas viejas y nuevas.

apps/api/src/migration/feature-flags.ts
export type FlagName =
  | 'tasks_standalone_ui'
  | 'tasks_zoneless'
  | 'orm_read_new_assignee_column';

export interface FlagContext {
  userId: string;
  tenantId: string;
  percentage: number; // 0..100 asignado de forma estable por hash
}

export function isEnabled(flag: FlagName, ctx: FlagContext): boolean {
  const rules: Record<FlagName, (c: FlagContext) => boolean> = {
    tasks_standalone_ui: (c) => c.percentage < 25 || c.tenantId === 'internal',
    tasks_zoneless: (c) => c.tenantId === 'internal',
    orm_read_new_assignee_column: (c) => c.percentage < 50,
  };
  return rules[flag](ctx);
}
Regla de oro

Si no puedes desactivar el cambio en menos de cinco minutos sin redeploy (flag remoto) o en un redeploy corto (flag en config), no estás listo para activarlo al 100 % de los usuarios de TaskFlow.

31.4.5 Criterios de decisión para el comité técnico

Cuando producto, operaciones y desarrollo discrepan («hay que reescribir», «no toquéis nada», «hagamos canary ya»), conviene anclar la conversación a criterios observables. Una plantilla que funciona en equipos universitarios y en empresas es puntuar del 1 al 5 cada eje y elegir la estrategia con menor riesgo residual, no la más elegante en un diagrama.

EjePreguntaSi puntúa bajo…Si puntúa alto…
Cobertura útil¿Los tests fallan cuando rompes crear/asignar tarea?No hagas big bang; invierte primero en smoke E2EPuedes acelerar saltos de major
Fidelidad staging¿Staging tiene datos y volumen parecidos a prod?Canary más agresivo en prod es imprudenteEnsayos de migración SQL confiables
Tolerancia SLA¿Cuántos minutos de degradación acepta el contrato?Flags + expand/contract obligatoriosVentana de mantenimiento posible
Acoplamiento front/back¿El SPA asume shapes exactos del JSON?Versiona API o haz cambios compatiblesPuedes canarear capas por separado
Capacidad del equipo¿Hay dueño claro del tren de migración?Reduce alcance; no abras cinco frentesStrangler por dominios en paralelo

En TaskFlow Legacy la puntuación típica fue: cobertura media, staging pobre, SLA estricto, acoplamiento alto, equipo de cuatro personas. La conclusión no fue «no migrar»: fue «Node+CI primero, Nest después, Angular a ritmo de una major por sprint, informes legacy estrangulados al final». Esa frase cabe en una diapositiva; el resto del capítulo es cómo ejecutarla sin romanticismo.

Analogía: obras en una estación de tren

Puedes cerrar la estación un mes (big bang), renovar andén a andén mientras circulan trenes (incremental), construir una estación nueva al lado y desviar líneas poco a poco (strangler), o instalar desvíos que puedes conmutar en segundos (feature flags). El viajero solo nota si llega a tiempo. Tu trabajo como arquitecto es elegir la obra que mantiene el servicio, no la que queda mejor en el portfolio.

31.5 Actualizar Angular (update.angular.dev, ng update, breaking changes)

Angular es el componente del stack con la mejor experiencia de actualización documentada. Úsala. El sitio update.angular.dev genera una checklist personalizada según versión origen/destino y opciones (Angular Material, UI Router, etc.). El CLI aplica muchos cambios mecánicos con ng update.

31.5.1 Flujo recomendado con ng update

terminal
# 1) Asegura working tree limpio y CI verde en main
git checkout -b chore/angular-13

# 2) Actualiza el propio CLI en la versión intermedia
npx @angular/cli@13 update @angular/core@13 @angular/cli@13

# 3) Ejecuta tests y smoke E2E
npm test -- --watch=false
npm run e2e:smoke

# 4) Repite major a major (14, 15, 16, 17...)
# No saltes "por ahorrar tiempo" si el generador recomienda pasos

Actualizar major a major parece lento; en la práctica es más rápido que depurar un salto de siete versiones donde se mezclan deprecaciones ya eliminadas, cambios de tsconfig y renombres de paquetes. Cada salto debe dejar main desplegable.

31.5.2 Breaking changes típicos que verás en TaskFlow

CambioDeAImpacto en TaskFlow
Motor de plantillasViewEngineIvy (ya default hace años)Si aún hay libs ViewEngine, bloquean; actualizar o reemplazar
MódulosNgModules omnipresentesStandalone por defectoMigración gradual (sección 31.8)
Control flow*ngIf/*ngFor@if/@forCodemod + revisión de track
ReactividadRxJS + async pipeSignals (+ RxJS donde aporte)Estados locales a signal; HTTP sigue cold observables
HttpHttpClientModuleprovideHttpClient()Interceptors funcionales (31.10)
TestsTestBed con módulosTestBed standalone / provide*Reescribir specs críticas
ViewEngine → Ivy: ya es historia (pero duele en libs)

Si tu app Angular 12+ ya corre Ivy, no «migras a Ivy»: ya estás. El problema real son dependencias publicadas solo como ViewEngine o con partial compilation antigua. El síntoma típico es un error de compilación opaco al subir de major. Solución: actualizar la lib, buscar alternativa, o encapsular el widget legado detrás de un wrapper hasta poder eliminarlo.

31.5.3 Control flow y signals: no los mezcles con el salto de major a ciegas

Una mala práctica habitual es, en el mismo PR que sube de Angular 15 a 17, reescribir todas las plantillas a @if/@for y convertir todos los componentes a signals. Estás mezclando tres ejes de cambio. Primero deja la app compilando y testeada en la nueva major; después aplica codemods de control flow por carpeta; después introduce signals en features nuevas o en islas acotadas.

task-list.component.htmlINCORRECTO
<!-- PR gigante: update + rewrite total -->
@for (task of tasksSignal(); track task.id) {
  @if (task.assignee(); as user) {
    <tf-task-row [task]="task" />
  }
}
task-list.component.htmlCORRECTO
<!-- Tras update estable: codemod por feature -->
@for (task of tasks; track task.id) {
  @if (task.assignee) {
    <tf-task-row [task]="task" />
  }
}

En el ejemplo «correcto» aún no hemos introducido signals: solo el nuevo control flow sobre el modelo imperativo/RxJS existente. El siguiente PR puede convertir tasks en tasks = signal<Task[]>([]) con calma.

apps/web/src/app/tasks/task-list.component.ts
import { Component, OnInit, inject, signal } from '@angular/core';
import { TaskApi } from './task-api.service';
import { Task } from './task.model';

@Component({
  selector: 'tf-task-list',
  standalone: true,
  templateUrl: './task-list.component.html',
})
export class TaskListComponent implements OnInit {
  private readonly api = inject(TaskApi);
  readonly tasks = signal<Task[]>([]);
  readonly error = signal<string | null>(null);

  ngOnInit(): void {
    this.api.list().subscribe({
      next: (items) => this.tasks.set(items),
      error: () => this.error.set('No se pudieron cargar las tareas'),
    });
  }
}

31.5.4 Material, CDK y el layout de assets

Si TaskFlow usa Angular Material, actualízalo en el mismo salto de major que el core cuando el schematic lo indique. Los temas, tipografías y tokens han cambiado entre eras (temas legacy frente a nuevos sistemas de theming). Además, el layout de salida de build (dist/...) ha variado entre majors: scripts de Nginx o CloudFront que asumen dist/browser o un index.html en una ruta concreta deben inventariarse en el apartado 31.3 y probarse en el pipeline, no el día del release.

infra/nginx/taskflow.conf
# Verifica tras cada major que la ruta real del index coincide
# Ejemplo orientativo; adáptalo al outputPath de angular.json
root /usr/share/nginx/html/browser;
try_files $uri $uri/ /index.html;

Otro detalle que tumba canaries «verdes» en infra pero rojos en negocio: los service workers y cachés agresivas de PWA (capítulo 27). Tras un update, fuerza un ciclo de actualización del worker en staging y documenta cómo invalidar caché si un usuario se queda con el bundle antiguo hablando con una API nueva.

Polyfills y zone.js en angular.json

Revisa polyfills y la importación de zone.js en cada salto. Algunas majors simplifican la lista de polyfills; dejar referencias rotas produce builds que solo fallan en CI con ciertos browserslist. Es un cambio «aburrido» que debe estar en el checklist del PR de update, junto a ng version pegado en la descripción.

31.6 Actualizar NestJS (Express/Fastify, breaking changes, peer deps)

NestJS evoluciona con más prudencia semántica que el front, pero los saltos de major siguen rompiendo tipos, adapters y peer dependencies. El error más caro en TaskFlow Legacy fue actualizar @nestjs/core sin alinear @nestjs/common, @nestjs/platform-express y los paquetes de microservicios/websockets que compartían versión.

31.6.1 Peer dependencies: actualiza el conjunto, no el paquete suelto

terminal
# Inventario de paquetes @nestjs/*
npm ls --depth=0 | grep @nestjs || true

# Actualización alineada (ejemplo a una major concreta; consulta el changelog)
npm install @nestjs/common@^10 @nestjs/core@^10 @nestjs/platform-express@^10 \
  @nestjs/config@^3 @nestjs/testing@^10 reflect-metadata rxjs

# Verifica peers
npm ls @nestjs/core
rxjs y reflect-metadata

Nest declara peers sobre RxJS y reflect-metadata. Un monorepo donde el front exige RxJS 7 y el back arrastra RxJS 6 «porque Nest 8» acaba con duplicados en el lockfile y bugs sutiles. Unifica versiones en el workspace antes de celebrar el update.

31.6.2 Express frente a Fastify en una migración

Cambiar de Express a Fastify no debe colarse en el mismo tren que el salto de major de Nest. Son ejes ortogonales. Express sigue siendo el default razonable para TaskFlow si tienes middleware Express puro (certain auth, multipart, legacy). Fastify aporta rendimiento y esquema JSON; exige adaptar middleware y, a veces, plugins de Nest.

main.tsINCORRECTO
// En el mismo PR: Nest 10 + Fastify + quitar middleware
const app = await NestFactory.create<NestFastifyApplication>(
  AppModule,
  new FastifyAdapter(),
);
// Se rompe el middleware de correlacion-id basado en Express
app.use(correlationMiddleware);
main.tsCORRECTO
// Primero: Nest 10 + Express estable
const app = await NestFactory.create(AppModule);
app.use(correlationMiddleware);

// Otro PR, más adelante: evaluar Fastify con spike y adapters

31.6.3 Breaking changes y deprecaciones que afectan a TaskFlow

apps/api/src/tasks/tasks.controller.ts
import { Controller, Get, Param, ParseUUIDPipe } from '@nestjs/common';
import { TasksService } from './tasks.service';
import { TaskDto } from './task.dto';

@Controller('tasks')
export class TasksController {
  constructor(private readonly tasks: TasksService) {}

  @Get(':id')
  findOne(@Param('id', ParseUUIDPipe) id: string): Promise<TaskDto> {
    return this.tasks.findOne(id);
  }
}

Tras actualizar Nest, ejecuta la suite del capítulo 13 (testing) completa: unitarios de servicios con mocked EntityManager, e2e con supertest y un PostgreSQL de CI. Si solo corres tsc --noEmit, te perderás roturas de runtime en filters y middleware.

31.7 Actualizar MikroORM 5→6 y migraciones de esquema

MikroORM 6 introduce cambios de API y de empaquetado que conviene tratar como un proyecto propio, acoplado al despliegue de Nest pero con checklist separada. La documentación oficial de upgrading es la fuente de verdad; aquí condensamos el impacto en TaskFlow.

31.7.1 Cambios de API que rompen compilaciones

Patrones frecuentes al pasar de 5 a 6 (verifica siempre la guía oficial de tu minor exacta):

apps/api/src/mikro-orm.config.ts
import { defineConfig } from '@mikro-orm/postgresql';
import { Migrator } from '@mikro-orm/migrations';
import { Task } from './tasks/task.entity';
import { User } from './users/user.entity';

export default defineConfig({
  entities: [Task, User],
  clientUrl: process.env.DATABASE_URL,
  migrations: {
    path: 'dist/migrations',
    pathTs: 'src/migrations',
  },
  extensions: [Migrator],
  // Nunca uses forceEntityConstructor / allowGlobalContext a la ligera en prod
});
tasks.service.tsINCORRECTO
// Asumir que el EntityManager "global" sigue igual tras el update
await this.em.flush();
const task = await this.em.findOneOrFail(Task, id, ['assignee']);
tasks.service.tsCORRECTO
const task = await this.em.findOneOrFail(
  Task,
  id,
  { populate: ['assignee'] },
);
await this.em.flush();

31.7.2 Migraciones de esquema: generar, revisar, aplicar

Nunca ejecutes en producción una migración generada sin revisión humana. El generador de esquemas es una ayuda, no un oráculo. En TaskFlow, una migración automática propuso DROP COLUMN porque alguien renombró un campo en la entidad sin mapear el nombre de columna antiguo.

terminal
# Desarrollo: genera a partir del diff de metadatos
npx mikro-orm migration:create --name rename_task_title

# Revisa el SQL generado en src/migrations/*.ts
# Aplica en local / CI
npx mikro-orm migration:up

# Estado
npx mikro-orm migration:pending
npx mikro-orm migration:list
apps/api/src/migrations/Migration20260731120000.ts
import { Migration } from '@mikro-orm/migrations';

export class Migration20260731120000 extends Migration {
  override async up(): Promise<void> {
    // Expand: columna nueva nullable primero (ver 31.12)
    this.addSql(
      `alter table "task" add column "title_v2" varchar(200) null;`,
    );
  }

  override async down(): Promise<void> {
    this.addSql(`alter table "task" drop column "title_v2";`);
  }
}
schema:update en producción

schema:update o sincronizar entidades contra prod es una anti-práctica. Solo migraciones versionadas, aplicadas por el pipeline (capítulo 21), con backup y plan de rollback.

31.7.3 Repositorio y tests tras el salto 5→6

Los tests que mockean EntityManager suelen ser el primer fuego tras actualizar MikroORM. Si tipabas métodos con firmas antiguas de find/findOne, TypeScript empezará a quejarse; si los mockeabas con as any, el runtime de integración fallará más tarde. Aprovecha el update para sustituir mocks frágiles por tests contra PostgreSQL efímero en los repositorios críticos (tareas, permisos, asignaciones).

apps/api/src/tasks/tasks.repository.spec.ts
import { MikroORM } from '@mikro-orm/core';
import config from '../mikro-orm.config';
import { Task } from './task.entity';

describe('TasksRepository (integración)', () => {
  let orm: MikroORM;

  beforeAll(async () => {
    orm = await MikroORM.init({
      ...config,
      clientUrl: process.env.TEST_DATABASE_URL,
      allowGlobalContext: true,
    });
    await orm.getSchemaGenerator().refreshDatabase();
  });

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

  it('persiste y recupera una tarea con populate', async () => {
    const em = orm.em.fork();
    const task = em.create(Task, {
      id: crypto.randomUUID(),
      title: 'Probar MikroORM 6',
    });
    await em.persistAndFlush(task);

    const found = await em.findOneOrFail(Task, task.id);
    expect(found.title).toBe('Probar MikroORM 6');
  });
});

Este estilo de test es más lento que un mock, pero es exactamente la red que necesitas cuando cambian detalles de persistencia. Reserva mocks para lógica de dominio pura; no para verificar que el ORM sigue mapeando columnas tras un upgrade.

31.8 Migrar de NgModules a standalone de forma segura

Standalone no es un interruptor binario. Angular permite convivir módulos y componentes standalone durante mucho tiempo. Esa convivencia es tu aliada: migra feature por feature, empieza por hojas del árbol (componentes presentacionales) y termina por el AppModule / bootstrap.

31.8.1 Secuencia segura en TaskFlow

  1. Marca componentes presentacionales como standalone: true e importa sus dependencias de plantilla en el propio decorador.
  2. Decláralos en los imports de los NgModules que aún los usan (sí, los módulos pueden importar standalone).
  3. Convierte rutas a loadComponent / loadChildren con arrays de rutas.
  4. Sustituye HttpClientModule por provideHttpClient() en bootstrap.
  5. Elimina AppModule cuando ya no aporte nada y usa bootstrapApplication.
  NgModule monolítico
        │
        │  1. hojas standalone
        ▼
  NgModule + imports standalone
        │
        │  2. rutas loadComponent
        ▼
  Shell module fino + features standalone
        │
        │  3. bootstrapApplication
        ▼
  App 100% standalone
apps/web/src/app/tasks/task-row.component.ts
import { Component, Input } from '@angular/core';
import { DatePipe } from '@angular/common';
import { Task } from './task.model';

@Component({
  selector: 'tf-task-row',
  standalone: true,
  imports: [DatePipe],
  template: `
    <article class="task-row">
      <h3>{{ task.title }}</h3>
      <time>{{ task.dueAt | date: 'shortDate' }}</time>
    </article>
  `,
})
export class TaskRowComponent {
  @Input({ required: true }) task!: Task;
}
apps/web/src/app/tasks/tasks.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TaskRowComponent } from './task-row.component';
import { TaskListComponent } from './task-list.component';

@NgModule({
  // TaskList aún no standalone: se declara
  declarations: [TaskListComponent],
  // TaskRow ya standalone: se importa
  imports: [CommonModule, TaskRowComponent],
  exports: [TaskListComponent],
})
export class TasksModule {}
Schematics oficiales

Angular ofrece schematics/codemods para convertir componentes a standalone. Úsalos, pero revisa el diff: a veces duplican imports o dejan módulos vacíos. El criterio de aceptación es «tests verdes + bundle no dispara warnings nuevos», no «el schematic terminó sin error».

31.8.2 TestBed durante la convivencia

task-row.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TaskRowComponent } from './task-row.component';

describe('TaskRowComponent', () => {
  let fixture: ComponentFixture<TaskRowComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [TaskRowComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(TaskRowComponent);
    fixture.componentInstance.task = {
      id: '1',
      title: 'Migrar standalone',
      dueAt: new Date('2026-08-01'),
    };
    fixture.detectChanges();
  });

  it('muestra el título', () => {
    const el: HTMLElement = fixture.nativeElement;
    expect(el.textContent).toContain('Migrar standalone');
  });
});

31.9 Migrar Zone.js → zoneless / signals gradualmente

Zone.js parchea APIs asíncronas para disparar change detection automáticamente. Funciona, pero oculta costes y complica SSR, microfrontends y debugging. El rumbo de Angular es zoneless con signals y notificaciones explícitas. La migración no es «quitar zone.js el viernes».

31.9.1 Enfoque gradual

  1. Introduce signals en estado local de UI (contadores, filtros, formularios simples).
  2. Sustituye patrones markForCheck / OnPush mal entendidos por datos reactivos claros.
  3. Activa zoneless en un entorno de desarrollo o detrás de flag interno.
  4. Corrige los componentes que dependían del «magic refresh» tras setTimeout o callbacks de librerías no parcheadas.
  5. Solo entonces elimines Zone del bootstrap de producción.
apps/web/src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

// Flag interno: solo staff TaskFlow
const zoneless = localStorage.getItem('tf_zoneless') === '1';

bootstrapApplication(AppComponent, {
  providers: [
    ...appConfig.providers,
    ...(zoneless ? [provideExperimentalZonelessChangeDetection()] : []),
  ],
}).catch(console.error);
APIs experimentales / estables

El nombre exacto del provider zoneless ha evolucionado entre versiones (experimental → estable). Consulta la documentación de tu major de Angular antes de copiar el símbolo. El principio didáctico permanece: activación gradual, signals primero, quitar Zone al final.

filters.component.tsINCORRECTO
// Depende de Zone para repintar tras el callback
export class FiltersComponent {
  query = '';
  onExternalSearch(cb: (q: string) => void): void {
    legacySearchWidget.onChange((q) => {
      this.query = q; // en zoneless puede no refrescar
      cb(q);
    });
  }
}
filters.component.tsCORRECTO
import { Component, signal } from '@angular/core';

export class FiltersComponent {
  readonly query = signal('');

  onExternalSearch(): void {
    legacySearchWidget.onChange((q: string) => {
      this.query.set(q); // notificación explícita
    });
  }
}

31.10 Sustituir HttpModule viejo, interceptores funcionales y TestBed

En Angular moderno, HttpClientModule se sustituye por provideHttpClient() y los interceptores de clase pueden (y suelen) migrarse a interceptores funcionales. En TaskFlow Legacy convivían un AuthInterceptor de clase, un RetryInterceptor a medias y tests que importaban el módulo completo.

31.10.1 Bootstrap con provideHttpClient

apps/web/src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import {
  provideHttpClient,
  withInterceptors,
} from '@angular/common/http';
import { APP_ROUTES } from './app.routes';
import { authInterceptor } from './http/auth.interceptor';
import { errorInterceptor } from './http/error.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(APP_ROUTES),
    provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])),
  ],
};
apps/web/src/app/http/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthStore } from '../auth/auth.store';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthStore).accessToken();
  if (!token) {
    return next(req);
  }
  return next(
    req.clone({
      setHeaders: { Authorization: `Bearer ${token}` },
    }),
  );
};
task-api.service.spec.tsINCORRECTO
await TestBed.configureTestingModule({
  imports: [HttpClientModule], // legado + trae interceptores reales
  providers: [TaskApi],
}).compileComponents();
task-api.service.spec.tsCORRECTO
import { provideHttpClient } from '@angular/common/http';
import {
  provideHttpClientTesting,
  HttpTestingController,
} from '@angular/common/http/testing';

await TestBed.configureTestingModule({
  providers: [
    TaskApi,
    provideHttpClient(),
    provideHttpClientTesting(),
  ],
}).compileComponents();

const http = TestBed.inject(HttpTestingController);

Si aún necesitas un interceptor de clase durante la transición, Angular permite withInterceptorsFromDi(). Úsalo como puente, no como destino. Documenta en el inventario qué interceptores faltan por convertir y apártanos un PR por interceptor (auth, error, correlacion-id).

apps/web/src/app/http/error.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { ToastService } from '../ui/toast.service';

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const toast = inject(ToastService);
  return next(req).pipe(
    catchError((err: unknown) => {
      if (err instanceof HttpErrorResponse && err.status >= 500) {
        toast.error('Error del servidor. Inténtalo de nuevo.');
      }
      return throwError(() => err);
    }),
  );
};

31.11 Codemods, reglas ESLint y ts-migrate

La migración mecánica no debe hacerse a mano archivo a archivo cuando existen transformaciones automatizadas. Un codemod es un script que reescribe el AST; una regla ESLint impide que el código nuevo reintroduzca el patrón viejo; herramientas como enfoques tipo ts-migrate ayudan a tipar bases JavaScript, aunque en TaskFlow (ya TypeScript) el valor está más en codemods de Angular/RxJS y en strictness gradual.

31.11.1 Codemods del ecosistema Angular

terminal
# Ejemplo: schematic de control flow (el nombre exacto depende de la CLI)
npx ng generate @angular/core:control-flow

# Revisa siempre el diff
git diff --stat
git diff apps/web/src/app/tasks | less

# Ejecuta tests de la feature tocada, no solo tsc
npm test -- --include='**/tasks/**/*.spec.ts'
Codemod ≠ revisión

Un codemod puede convertir *ngFor="let t of tasks; trackBy: track" en un @for con track subóptimo, o dejar *ngIf as as mal migrados. Presupuesto tiempo de revisión proporcional al tamaño del diff, no al tiempo que tardó el comando.

31.11.2 ESLint como barandilla post-migración

Tras migrar una carpeta a standalone o a interceptores funcionales, añade reglas que fallen en CI si alguien vuelve a importar el patrón prohibido.

eslint.config.mjs (extracto)
// Pseudoconfig: adapta a flat config o .eslintrc según el repo
export default [
  {
    files: ['apps/web/src/app/**/*.ts'],
    rules: {
      'no-restricted-imports': [
        'error',
        {
          paths: [
            {
              name: '@angular/common/http',
              importNames: ['HttpClientModule'],
              message:
                'Usa provideHttpClient() en el bootstrap (ver cap. 31).',
            },
            {
              name: 'rxjs',
              importNames: ['toPromise'],
              message: 'Usa firstValueFrom / lastValueFrom.',
            },
          ],
        },
      ],
    },
  },
];
apps/web/src/app/tasks/task-api.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { Task } from './task.model';

@Injectable({ providedIn: 'root' })
export class TaskApi {
  private readonly http = inject(HttpClient);

  list(): Promise<Task[]> {
    return firstValueFrom(this.http.get<Task[]>('/api/tasks'));
  }
}

31.11.3 ts-migrate y strictness gradual

Si alguna parte de TaskFlow sigue en JavaScript (scripts de tooling, un panel admin antiguo), herramientas al estilo ts-migrate insertan anotaciones $TSFixMe / any para conseguir compilación tipada y luego ir cerrando agujeros. En código ya TypeScript, el análogo es activar flags uno a uno:

  1. noImplicitAny
  2. strictNullChecks
  3. strictFunctionTypes
  4. noUncheckedIndexedAccess (más agresivo; al final)
scripts/strict-progress.sh
#!/usr/bin/env bash
# Cuenta errores de tsc como métrica de migración tipada
npx tsc -p apps/api/tsconfig.json --noEmit --pretty false 2>&1 \
  | tee /tmp/tsc-api.txt \
  | wc -l

31.12 Bases de datos: migraciones zero-downtime y expand/contract

El código puede desplegarse en minutos; el esquema compartido entre instancias viejas y nuevas durante un rolling deploy es el verdadero cuello de botella. El patrón expand/contract (también llamado parallel change) evita downtime y rollbacks imposibles.

  Expand / Contract sobre columna assignee

  1. EXPAND
     task.assignee_id (vieja)     task.assignee_uuid (nueva, nullable)
            │                              │
            └──── app escribe en AMBAS ────┘
                  (o trigger/backfill)

  2. MIGRATE READS
     lecturas pasan a assignee_uuid detrás de flag
     backfill batch de filas históricas

  3. CONTRACT
     se deja de escribir en assignee_id
     se elimina columna vieja en migración posterior

31.12.1 Fase expand

apps/api/src/migrations/Migration20260801100000.ts
import { Migration } from '@mikro-orm/migrations';

export class Migration20260801100000 extends Migration {
  override async up(): Promise<void> {
    this.addSql(
      `alter table "task" add column "assignee_uuid" uuid null;`,
    );
    this.addSql(
      `create index "task_assignee_uuid_idx" on "task" ("assignee_uuid");`,
    );
  }

  override async down(): Promise<void> {
    this.addSql(`drop index "task_assignee_uuid_idx";`);
    this.addSql(`alter table "task" drop column "assignee_uuid";`);
  }
}
apps/api/src/tasks/task.entity.ts
import { Entity, PrimaryKey, Property, ManyToOne } from '@mikro-orm/core';
import { User } from '../users/user.entity';

@Entity({ tableName: 'task' })
export class Task {
  @PrimaryKey({ type: 'uuid' })
  id!: string;

  @Property()
  title!: string;

  /** Columna legada; no leer en código nuevo */
  @ManyToOne(() => User, { nullable: true, fieldName: 'assignee_id' })
  assigneeLegacy?: User;

  @Property({ nullable: true, fieldName: 'assignee_uuid' })
  assigneeUuid?: string;
}

31.12.2 Backfill y doble escritura

apps/api/src/tasks/tasks.service.ts
async assign(taskId: string, userId: string): Promise<void> {
  const task = await this.em.findOneOrFail(Task, taskId);
  const user = await this.em.findOneOrFail(User, userId);

  // Doble escritura durante expand
  task.assigneeLegacy = user;
  task.assigneeUuid = user.id;

  await this.em.flush();
}

async backfillAssignees(batchSize = 500): Promise<number> {
  const pending = await this.em.find(
    Task,
    { assigneeUuid: null, assigneeLegacy: { $ne: null } },
    { limit: batchSize, populate: ['assigneeLegacy'] },
  );
  for (const task of pending) {
    task.assigneeUuid = task.assigneeLegacy!.id;
  }
  await this.em.flush();
  return pending.length;
}
Locks y tablas grandes

Un UPDATE task SET assignee_uuid = ... masivo en una tabla de decenas de millones de filas puede bloquear producción. Prefiere batches, ventanas de bajo tráfico y supervisión de pg_stat_activity. Coordina con el DBA (capítulo 21).

31.12.3 Fase contract

Solo cuando el 100 % del tráfico lee y escribe la columna nueva, y el backfill ha terminado, eliminas la columna vieja. Ese deploy de código que deja de referenciar assigneeLegacy debe ocurrir antes del DROP COLUMN.

PasoCódigoEsquemaRollback
Expanddoble escrituraADD nullableDROP columna nueva
Migrate readsflag de lecturasin cambioapagar flag
Stop legacy writesolo columna nuevasin cambioreactiviar doble escritura
Contractborrar campo legadoDROP columna viejadifícil: restaurar backup

31.13 Plan de rollback y canary

Si no tienes rollback escrito antes del deploy, no tienes un plan de migración: tienes una esperanza. El canary reduce el radio de explosión; el rollback limita la duración del incidente.

31.13.1 Canary de TaskFlow

  Tráfico usuarios TaskFlow
           │
           ▼
     ┌─────────────┐
     │  balanceador│
     └──────┬──────┘
            │
     ┌──────┴──────┐
     │ 10% canary  │──── pods api:nest10-mikro6 + web:angular-nueva
     │ 90% estable │──── pods versión anterior
     └─────────────┘
            │
            ▼
     métricas: 5xx, latencia p95, error JS (Sentry),
     login success rate, "crear tarea" success

31.13.2 Rollback: qué es reversible y qué no

CambioRollback típicoNotas
Deploy API / WebRedeploy imagen anteriorMinutos si el artefacto sigue en el registry
Feature flagApagar flagSegundos; ideal
Migración expand (ADD)DROP columna nueva o ignorarlaSeguro si nullable y sin deps
Migración contract (DROP)Restaurar backup / forward fixEvita llegar aquí sin certeza
Migración destructiva de datosBackup point-in-timeEnsayar restore en staging
docs/RUNBOOK_MIGRATION_ROLLBACK.md
# Runbook resumido TaskFlow — canary API nest10

1. Observabilidad: dashboard "release-canary" (5xx, p95, Sentry).
2. Si 5xx > 1% durante 5 min O error JS x3 baseline:
   a. Apagar flag tasks_standalone_ui (si aplica).
   b. kubectl rollout undo deploy/taskflow-api-canary
   c. Verificar health /readyz en pods estables.
3. Si la migración SQL expand se aplicó:
   - NO ejecutar contract.
   - Decidir si DROP columna nueva o dejarla inerte.
4. Abrir incidente, postmortem en 48h (blameless).
Ensayo de rollback

En staging, el equipo de TaskFlow ejecuta un «game day»: despliega canary, fuerza un fallo, y mide el tiempo hasta tráfico 100 % estable otra vez. Si supera el objetivo (p. ej. 15 minutos), el release a producción se pospone.

31.14 Checklist de release post-migración

Usa esta lista como puerta de calidad antes de declarar la major «terminada». No es cosmética: cada ítem evita una clase de incidente que ya hemos visto en proyectos reales.

  • Inventario actualizado en MIGRATION_INVENTORY.md con versiones finales.
  • Node LTS alineada en local, CI, Docker y producción.
  • npm audit sin críticas explotables sin mitigación documentada.
  • CI verde: unit, integration, e2e smoke, migraciones contra PG efímero.
  • Contrato OpenAPI / tests de contrato sin drift no intencional.
  • Migraciones SQL revisadas; expand aplicado; contract aplazado si procede.
  • Feature flags con dueño y fecha de limpieza (evitar flags eternos).
  • Canary definido con métricas de negocio, no solo infra.
  • Runbook de rollback enlazado en el ticket de release.
  • Dashboards y alertas revisados (umbrales válidos tras el cambio de latencia baseline).
  • Documentación de onboarding (README) con nuevos comandos CLI.
  • Comunicación a soporte: qué cambia para el usuario (aunque sea «nada visible»).
.github/workflows/migration-gate.yml
name: migration-gate
on:
  pull_request:
    paths:
      - 'apps/**'
      - 'package.json'
      - 'package-lock.json'
jobs:
  gate:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: taskflow
        ports: ['5432:5432']
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run test:api
      - run: npm run test:web
      - run: npm run mikro:migrate:up
      - run: npm run e2e:smoke

31.15 Errores comunes y cómo solucionarlos

Error / síntomaCausa probableSolución
Peer dependency warnings y runtime crash NestMajors @nestjs/* desalineadasInstalar el conjunto completo de paquetes Nest a la misma major
Build Angular falla en librería UILib aún ViewEngine / incompatible IvyActualizar lib, sustituirla o aislarla
Tests HttpClient rompen tras updateSigue HttpClientModule + interceptores DIprovideHttpClient + provideHttpClientTesting
UI no refresca tras quitar ZoneCallbacks externos sin signalsEnvolver estado en signals o notificar CD explícita
Migración MikroORM hace DROP inesperadoDiff de entidad sin mapear fieldNameRevisar SQL; mapear columnas; expand/contract
Canary «verde» pero negocio caídoMétricas solo de infraSLIs de crear/asignar tarea, login, checkout
ng update deja el repo a mediasConflictos / working tree sucioRama limpia, major a major, commit por salto
Docker prod con Node 16 y app en 20Inventario incompletoMisma LTS en todos los entornos
Rollback imposible tras DROP COLUMNContract prematuroNo contraer hasta lecturas 100 % nuevas
Strict NullChecks tras update = 2000 erroresSe activó todo a la vezFlags graduales + métrica de errores tsc
package.jsonINCORRECTO
{
  "dependencies": {
    "@nestjs/common": "10.3.0",
    "@nestjs/core": "8.4.7",
    "@nestjs/platform-express": "9.4.0"
  }
}
package.jsonCORRECTO
{
  "dependencies": {
    "@nestjs/common": "10.3.0",
    "@nestjs/core": "10.3.0",
    "@nestjs/platform-express": "10.3.0"
  }
}

31.16 Buenas y malas prácticas

  • Inventariar versiones, tests y bloqueadores antes de tocar código.
  • Actualizar Node LTS como prerrequisito.
  • Subir Angular major a major con CI verde en cada salto.
  • Separar ejes: major, standalone, signals, zoneless, Fastify.
  • Usar feature flags y canary con métricas de negocio.
  • Migraciones SQL expand/contract revisadas por humanos.
  • Barandillas ESLint contra patrones legados.
  • Escribir el runbook de rollback antes del release.
  • Limpiar flags y columnas legadas con fecha límite.
  • Documentar el estado en el repo, no solo en chats.
  • Reescribir el producto «ya que actualizamos».
  • Saltar siete majors de Angular en un solo PR.
  • Mezclar Fastify + Nest major + cambio de esquema.
  • Usar schema:update en producción.
  • Desactivar strict para que compile.
  • Confiar en codemods sin revisar el diff.
  • Declarar éxito solo porque el deploy terminó.
  • Hacer DROP COLUMN en el mismo deploy que el código nuevo.
  • Dejar HttpClientModule «porque los tests pasan en local».
  • Acumular una rama de migración de tres meses.

31.17 Preguntas frecuentes

¿Debo actualizar Angular y NestJS en el mismo sprint?

Solo si el equipo es pequeño, la cobertura es excelente y el contrato HTTP entre SPA y API no cambia. En TaskFlow con varios squads, es más seguro estabilizar la API (Nest + MikroORM + Node) y después subir Angular, o al revés si el riesgo está en el front. Lo que no debes hacer es mezclar en el mismo PR un salto de major de ambos lados con un cambio de esquema destructivo. El criterio es el radio de explosión del rollback: si un fallo te obliga a revertir front y back a la vez, el tren era demasiado grande.

¿Cuántas majors de Angular puedo saltar de una vez?

El generador de update.angular.dev permite planificar saltos, pero la práctica recomendada en productos vivos es avanzar major a major (o en los tramos que el propio checklist agrupe con schematics maduros), dejando CI verde y un deploy a staging en cada estación. Saltar de 12 a 19 en una rama de tres meses acumula conflictos y hace imposible atribuir regresiones. Si el calendario aprieta, paraleliza preparación (quitar libs bloqueantes, activar strict) con los saltos, no fusiones todos los breaking changes en un único diff inhumano.

¿NgModules están «prohibidos» en Angular moderno?

No. Siguen soportados y son perfectamente válidos durante años de convivencia. Lo que cambia es el default de proyectos nuevos y la dirección de la documentación. En una migración, tratar NgModules como ilegales de la noche a la mañana obliga a reescribir tests, rutas y lazy loading sin beneficio inmediato. Migra por features, mide bundle y DX, y elimina el AppModule cuando deje de aportar estructura. Prohibir módulos por dogma es tan dañino como aferrarse a ellos por miedo.

¿Signals sustituyen por completo a RxJS?

No. Signals brillan en estado síncrono de UI y en derivaciones locales. RxJS sigue siendo la herramienta adecuada para eventos en el tiempo, composición de streams HTTP, websockets, debounce de búsquedas y cancelación. El error de migración es reescribir cada Observable a signals por moda. El acierto es: estado de componente → signals; fronteras asíncronas y pipelines → RxJS; puentes explícitos (toSignal, toObservable) cuando haga falta. Ver también el capítulo 4 sobre reactividad.

¿Cuándo quito Zone.js?

Cuando (1) el estado crítico de UI ya notifica con signals o CD explícita, (2) has activado zoneless en un canary interno sin regresiones de pintado, (3) las librerías de terceros que usan callbacks no parcheados están encapsuladas, y (4) tienes tests e2e que cubren los flujos que antes «se refrescaban solos». Quitar Zone para ganar un benchmark sintético sin esa preparación suele generar bugs intermitentes imposibles de reproducir en unit tests.

¿MikroORM 6 obliga a reescribir todas las entidades?

Habitualmente no. Muchas entidades migran con cambios menores de imports/config. Lo que sí exige revisión cuidadosa son populate tipados, serialización, configuración de migraciones y cualquier uso de APIs marcadas como breaking en la guía oficial 5→6. El trabajo pesado suele estar en el EntityManager de servicios y en los tests que mockeaban APIs antiguas, no en cada propiedad decorada. Lee siempre el upgrading guide de la versión exacta a la que vas.

¿Puedo usar feature flags para cambios de esquema SQL?

Las flags controlan el comportamiento de la aplicación (qué columna lee, si hace doble escritura). No sustituyen el diseño expand/contract del esquema. Una flag no puede «deshacer» un DROP COLUMN ya aplicado en producción. Usa flags para conmutar lecturas/escrituras durante la fase expand; usa migraciones versionadas para el DDL; usa backups y ensayos de restore para el peor caso. Mezclar ambos conceptos en la cabeza del equipo produce falsas sensaciones de seguridad.

¿Qué hago con una dependencia que ya no soporta mi major de Angular?

Tres caminos, en orden: (1) actualizar o sustituir por una alternativa mantenida; (2) encapsular el widget en un wrapper y aislar el riesgo (incluso un custom element si el vendor publica fuera de Angular); (3) fork temporal con fecha de caducidad y dueño. Lo que no debes hacer es congelar toda la aplicación en una major antigua por un datepicker. El inventario del apartado 31.3 debe listar estos bloqueadores antes de planificar fechas de release.

¿El canary del front y del back deben ser el mismo porcentaje?

No necesariamente. Si mantienes compatibilidad de contrato (API versionada o al menos retrocompatible), puedes canarear la API al 10 % y el front al 5 %, o estabilizar uno al 100 % antes de mover el otro. Lo delicado es canarear un front nuevo que exige un campo que solo la API canary conoce: ahí has acoplado los dos despliegues y el rollback se complica. Diseña contratos tolerantes durante la migración.

¿Cómo convenzo a producto de invertir en actualizar si «no hay features»?

Traduce a riesgo de negocio: CVEs sin parche, imposibilidad de contratar (el talento no quiere Node 14), bloqueo de features que dependen de APIs nuevas, coste creciente de cada hotfix, y tiempo de onboarding. Presenta un plan por fases con entregables visibles (CI más rápida, pipeline de migraciones, eliminación de una lib abandonada) en lugar de un epico monstruoso «Migración Angular 19». Producto entiende hitos; no entiende «refactor técnico» infinito.

¿Debo activar strict antes o después del update del framework?

Idealmente, reduce la deuda tipada antes o en paralelo temprano: cada any es un error de runtime esperando al breaking change. Si el muro de errores es inasumible, activa flags de strictness por paquetes (apps/api primero, luego apps/web) y mide el número de errores de tsc como KPI. Desactivar strict «para salir del paso» tras un ng update es la forma más rápida de introducir regresiones silenciosas.

¿Qué diferencia hay entre rollback de código y forward fix?

Rollback vuelve al artefacto anterior; forward fix despliega un parche encima. Tras un DROP COLUMN o una migración de datos destructiva, el rollback de código puede ser insuficiente porque el esquema ya no coincide con el binario viejo. Por eso expand/contract prioriza cambios reversibles y reserva el contract para cuando el forward path está consolidado. En el runbook, deja explícito cuándo está permitido cada modo.

¿Los interceptores funcionales pueden inyectar servicios con estado?

Sí, mediante inject() dentro de la función, con las mismas reglas que en otros contextos de inyección de Angular. Evita leer estado mutable global no reactivo de forma opaca; prefiere stores explícitos (como el AuthStore del ejemplo). En tests, usa provideHttpClient(withInterceptors([...])) o testing providers para sustituir dependencias. Si un interceptor necesita un ciclo de vida complejo, reconsidera si parte de la lógica debería vivir en un servicio llamado por el interceptor, no al revés.

¿Es obligatorio pasar de Express a Fastify al actualizar Nest?

No. Es una decisión de rendimiento y ecosistema aparte. Muchos TaskFlow en producción siguen con Express tras Nest 10+ sin drama. Evalúa Fastify cuando midas cuellos de botella reales en serialización/HTTP y cuando no dependas de middleware Express imposible de adaptar. Meter Fastify «porque sale en un blog» dentro del tren de migración es una fuente clásica de retrasos.

31.18 Ejercicios

Nivel 1 · básico

Ejercicio 1. Redacta el inventario inicial de un TaskFlow ficticio con Angular 13, Nest 9, MikroORM 5 y Node 16. Incluye al menos ocho filas (runtime, libs, tests, bloqueadores).

Nivel 1 · básico

Ejercicio 2. Escribe los comandos, en orden, para subir un proyecto Angular de la major 15 a 16 con ng update, incluyendo verificación de tests.

Nivel 1 · básico

Ejercicio 3. Convierte mentalmente (o en un sandbox) un interceptor de clase que añade Authorization a un HttpInterceptorFn. Lista tres asserts que pondrías en el test con HttpTestingController.

Nivel 1 · básico

Ejercicio 4. Explica con tus palabras la diferencia entre big bang, incremental y strangler fig aplicados a la ruta /reports de TaskFlow.

Nivel 2 · intermedio

Ejercicio 5. Diseña una migración expand/contract para renombrar task.status_code (varchar) a task.status (enum PG). Detalla SQL de expand, estrategia de backfill, flag de lectura y momento del contract.

Nivel 2 · intermedio

Ejercicio 6. Dado un TasksModule con tres componentes, describe el orden exacto para pasarlos a standalone sin romper el lazy loading actual.

Nivel 2 · intermedio

Ejercicio 7. Propón métricas de canary (al menos cinco) para un release que cambia solo el módulo de autenticación Nest. Incluye umbrales de aborto.

Nivel 2 · intermedio

Ejercicio 8. Redacta una regla no-restricted-imports que prohíba HttpClientModule y toPromise, con mensajes orientados a TaskFlow.

Nivel 3 · avanzado

Ejercicio 9. Planifica un calendarío de 10 semanas para llevar TaskFlow Legacy (Angular 12, Nest 8, MikroORM 5, Node 16) hasta el stack objetivo, con hitos fusionables a main cada semana.

Nivel 3 · avanzado

Ejercicio 10. Escribe un runbook de rollback para un canary que ya aplicó una migración expand (ADD COLUMN) y detecta 2 % de 5xx en POST /tasks.

Nivel 3 · avanzado

Ejercicio 11. Diseña la activación zoneless detrás de flag por tenant, incluyendo cómo validarás regresiones de change detection en Playwright.

Nivel 3 · avanzado

Ejercicio 12. Compara riesgos de actualizar MikroORM 5→6 antes que Nest 8→10 versus al revés. Elabora una recomendación justificada para TaskFlow con 200 tests de repositorio.

Solución comentada · Ejercicio 2 (ng update 15→16)

Orden seguro:

solución
git checkout -b chore/angular-16
git status  # working tree limpio
npx @angular/cli@16 update @angular/core@16 @angular/cli@16
# Si usas Material u otras libs oficiales:
# npx ng update @angular/material@16
npm test -- --watch=false
npm run e2e:smoke
git add -A && git commit -m "chore(web): Angular 15 a 16"

Si el schematic falla a medias, no «apañes» a mano y sigas: revierte la rama, corrige el bloqueador (lib, tsconfig) y relanza. Cada salto debe ser un commit desplegable.

Solución comentada · Ejercicio 5 (expand/contract status)

Expand: ADD COLUMN status varchar(...) o el tipo enum con valor NULL; mantener status_code. Doble escritura en el servicio al crear/actualizar tareas. Backfill por batches mapeando códigos legados al enum. Lecturas: flag orm_read_new_status al 0 % → 10 % → 50 % → 100 %. Stop writes a status_code. Contract: migración que hace DROP COLUMN status_code solo tras un periodo de observación. El rollback antes del contract es apagar la flag y, si hace falta, redeploy; después del contract, forward fix o restore.

Solución comentada · Ejercicio 10 (runbook)

1) Confirmar el síntoma en dashboard (5xx en POST /tasks). 2) Apagar flags de UI asociados si el front canary participa. 3) rollout undo del deploy canary de API. 4) Verificar que el 90 % estable absorbe tráfico y /readyz OK. 5) No ejecutar contract ni DROP; la columna nueva puede quedarse nullable. 6) Si se sospecha corrupción de datos en escrituras dobles, lanzar job de verificación de invariantes. 7) Congelar nuevos deploys, abrir incidente, capturar logs/traces del canary, postmortem en 48 h con acción preventiva (test de contrato faltante, etc.).

31.19 Resumen del capítulo

  • La deuda de no actualizar se compone: cada major saltada encarece la siguiente.
  • Empieza por inventario (Node LTS, frameworks, tests, bloqueadores), no por ng update.
  • Elige estrategia (incremental, strangler, flags) según radio de explosión y cobertura.
  • Angular: update.angular.dev + major a major; separa standalone, control flow, signals y zoneless.
  • Nest: alinea peers; no mezcles Fastify en el mismo tren sin necesidad.
  • MikroORM 5→6: sigue la guía oficial; migraciones SQL revisadas; nunca schema sync en prod.
  • Standalone y zoneless se migran por islas, con convivencia y flags.
  • Http: provideHttpClient + interceptores funcionales + TestBed moderno.
  • Codemods aceleran; ESLint impide regresiones; strict tipado es red de seguridad.
  • DB: expand/contract + backfill + contract tardío.
  • Canary con métricas de negocio y runbook de rollback escritos antes del release.
  • La migración no termina en el merge: termina cuando el canary está estable, el contract se ejecutó y el runbook se validó con un ensayo.
  • «Si funciona no lo toques» suele significar «no sabemos restaurarlo»; el inventario y los tests son lo que convierten el miedo en plan.
Criterio senior

Actualizar no es un acto de valentía: es un proceso de ingeniería. Quien propone un salto de tres majors de Angular en un PR sin inventario, sin tests de contrato y sin plan de expand/contract no está siendo ágil; está externalizando el riesgo al turno de guardia. TaskFlow se actualiza en trenes pequeños, medibles y reversibles.

Guarda plantillas reutilizables: checklist de inventario, plantilla de ADR para elegir strangler frente a big bang, plantilla de runbook de rollback, y un tablero con el estado de cada app/lib respecto a la versión objetivo. El coste de mantener esas plantillas es ridículo frente al de un fin de semana entero apagando fuegos tras un ng update a ciegas.

En la entrevista, distingue actualización de framework (tooling + breaking changes) de migración de arquitectura (standalone, zoneless, módulos Nest). Son ejes distintos: puedes estar en Angular reciente con NgModules, o a medias en standalone con una versión vieja. El plan debe decir qué eje se mueve en cada tren y qué métrica indica éxito (tests, bundle, error rate, tiempo de build).

Cierra el círculo con el capítulo 13 (tests como red de seguridad), el 21 (despliegue y probes) y el 30 (monorepo affected): sin esas tres piezas, cada actualización es un salto de fe. Con ellas, es mantenimiento rutinario — aburrido, predecible y barato a largo plazo, que es exactamente lo que quieres en producción.

31.20 Recursos adicionales

Cierre

Migrar TaskFlow no es un acto de valentía heroica de un fin de semana: es un proceso de ingeniería con inventario, trenes pequeños, barandillas y la humildad de poder dar marcha atrás. Si aplicas este capítulo, la frase «si funciona no lo toques» se transforma en «lo tocamos a menudo, con red, para que siga funcionando».