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.
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 updatey 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».
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:
- Dependencias. Paquetes anclados a majors antiguas, resoluciones forzadas en
package.json,overrides/resolutionsque esconden incompatibilidades. - Plataforma. Node 14 o 16 fuera de LTS, PostgreSQL sin actualizaciones de seguridad, imágenes Docker basadas en Alpine con OpenSSL incompatible.
- Arquitectura de aplicación. NgModules monolíticos, servicios con estado global, entidades MikroORM con relaciones cargadas siempre en eager, tests E2E que solo pasan en la máquina de un compañero.
- Proceso. Ausencia de changelog interno, releases manuales, sin canary, sin feature flags, sin métricas de error post-despliegue (ver capítulo 21).
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, sí 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:
- El
npm auditmuestra vulnerabilidades críticas en dependencias transitivas que ya no reciben parches en la major actual. - 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.
- Una librería de UI esencial abandona Angular 12; el equipo empieza a forkearla «temporalmente».
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.
#!/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
| Área | TaskFlow Legacy (ejemplo) | Objetivo razonable | Riesgo si se ignora |
|---|---|---|---|
| Node | 16.20 | LTS activa (p. ej. 20 o 22) | Imágenes CI rotas, paquetes sin binarios |
| Angular | 12.2 | Major actual o LTS del equipo | Sin seguridad, sin standalone/signals |
| NestJS | 8.x | Major actual alineada con Node | Peer deps rotos, Express tipado antiguo |
| MikroORM | 5.x | 6.x | API de EntityManager/migraciones distinta |
| TypeScript | 4.3, strict parcial | 5.x con strict: true | Regresiones silenciosas al tipar más |
| RxJS | 6.x | 7.x | Operadores y toPromise deprecados |
| PostgreSQL | 13 | 15/16 según ops | Features y parches; planificar con DBA |
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.
{
"compilerOptions": {
"strict": false,
"noImplicitAny": false,
"skipLibCheck": true,
"suppressImplicitAnyIndexErrors": true
}
}
{
"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:
- Tests unitarios de servicios de dominio TaskFlow (tareas, proyectos, permisos).
- Tests de contrato HTTP (Pact o al menos snapshots de OpenAPI) entre SPA y API.
- Una suite E2E smoke (login, crear tarea, asignar, completar) automatizada en CI.
- Migraciones MikroORM aplicadas en CI contra PostgreSQL efímero (Testcontainers o servicio Compose).
# 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.
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):
- Node LTS + herramientas de build (CLI, webpack/esbuild/vite según versión).
- TypeScript strict y limpieza de
anycríticos. - NestJS + MikroORM en la API (contratos HTTP estables).
- Angular major a major (o siguiendo el generador oficial de saltos).
- Refactors estructurales: standalone, control flow, signals, zoneless.
- 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.
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.
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);
}
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.
| Eje | Pregunta | Si 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 E2E | Puedes acelerar saltos de major |
| Fidelidad staging | ¿Staging tiene datos y volumen parecidos a prod? | Canary más agresivo en prod es imprudente | Ensayos de migración SQL confiables |
| Tolerancia SLA | ¿Cuántos minutos de degradación acepta el contrato? | Flags + expand/contract obligatorios | Ventana de mantenimiento posible |
| Acoplamiento front/back | ¿El SPA asume shapes exactos del JSON? | Versiona API o haz cambios compatibles | Puedes canarear capas por separado |
| Capacidad del equipo | ¿Hay dueño claro del tren de migración? | Reduce alcance; no abras cinco frentes | Strangler 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.
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
# 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
| Cambio | De | A | Impacto en TaskFlow |
|---|---|---|---|
| Motor de plantillas | ViewEngine | Ivy (ya default hace años) | Si aún hay libs ViewEngine, bloquean; actualizar o reemplazar |
| Módulos | NgModules omnipresentes | Standalone por defecto | Migración gradual (sección 31.8) |
| Control flow | *ngIf/*ngFor | @if/@for | Codemod + revisión de track |
| Reactividad | RxJS + async pipe | Signals (+ RxJS donde aporte) | Estados locales a signal; HTTP sigue cold observables |
| Http | HttpClientModule | provideHttpClient() | Interceptors funcionales (31.10) |
| Tests | TestBed con módulos | TestBed standalone / provide* | Reescribir specs críticas |
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.
<!-- PR gigante: update + rewrite total -->
@for (task of tasksSignal(); track task.id) {
@if (task.assignee(); as user) {
<tf-task-row [task]="task" />
}
}
<!-- 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.
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.
# 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.
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
# 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
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.
// 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);
// 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
- Versiones de TypeScript mínimas por major: el compilador del API debe subir con Nest.
- Cambios en tipado de
ExecutionContext, guards y pipes personalizados. - Módulos de terceros (
@nestjs/passport, Swagger, Schedule) con majors propias: léelos en el mismo checklist. - Configuración de validación (
ValidationPipe) y transformacionesclass-transformercuando actualizas esas libs en paralelo.
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):
- Ajustes en imports de drivers y paquetes reflejo (
@mikro-orm/postgresql, etc.). - Cambios en la configuración (
defineConfig, opciones renombradas). - Diferencias en el manejo de
WrappedEntity, populate y serialización. - CLI de migraciones: asegúrate de que el mismo config usa la app y la CLI.
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
});
// Asumir que el EntityManager "global" sigue igual tras el update
await this.em.flush();
const task = await this.em.findOneOrFail(Task, id, ['assignee']);
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.
# 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
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 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).
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
- Marca componentes presentacionales como
standalone: truee importa sus dependencias de plantilla en el propio decorador. - Decláralos en los
importsde los NgModules que aún los usan (sí, los módulos pueden importar standalone). - Convierte rutas a
loadComponent/loadChildrencon arrays de rutas. - Sustituye
HttpClientModuleporprovideHttpClient()en bootstrap. - Elimina
AppModulecuando ya no aporte nada y usabootstrapApplication.
NgModule monolítico
│
│ 1. hojas standalone
▼
NgModule + imports standalone
│
│ 2. rutas loadComponent
▼
Shell module fino + features standalone
│
│ 3. bootstrapApplication
▼
App 100% standalone
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;
}
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 {}
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
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
- Introduce signals en estado local de UI (contadores, filtros, formularios simples).
- Sustituye patrones
markForCheck/OnPushmal entendidos por datos reactivos claros. - Activa zoneless en un entorno de desarrollo o detrás de flag interno.
- Corrige los componentes que dependían del «magic refresh» tras
setTimeouto callbacks de librerías no parcheadas. - Solo entonces elimines Zone del bootstrap de producción.
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);
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.
// 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);
});
}
}
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
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])),
],
};
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}` },
}),
);
};
await TestBed.configureTestingModule({
imports: [HttpClientModule], // legado + trae interceptores reales
providers: [TaskApi],
}).compileComponents();
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).
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
- Schematics de
ng update: renombres, imports,tsconfig. - Migraciones a standalone y a nuevo control flow publicadas con el CLI (según versión).
- Codemods comunitarios para RxJS (sustituir
toPromiseporfirstValueFrom/lastValueFrom).
# 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'
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.
// 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.',
},
],
},
],
},
},
];
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:
noImplicitAnystrictNullChecksstrictFunctionTypesnoUncheckedIndexedAccess(más agresivo; al final)
#!/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
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";`);
}
}
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
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;
}
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.
| Paso | Código | Esquema | Rollback |
|---|---|---|---|
| Expand | doble escritura | ADD nullable | DROP columna nueva |
| Migrate reads | flag de lectura | sin cambio | apagar flag |
| Stop legacy write | solo columna nueva | sin cambio | reactiviar doble escritura |
| Contract | borrar campo legado | DROP columna vieja | difí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
- Promociona canary solo si las métricas de negocio (crear tarea, asignar) se mantienen.
- No mires solo CPU: un 500 en
POST /taskscon CPU baja es un canary fallido. - El front y el back pueden canarearse por separado si el contrato HTTP es compatible (preferible).
31.13.2 Rollback: qué es reversible y qué no
| Cambio | Rollback típico | Notas |
|---|---|---|
| Deploy API / Web | Redeploy imagen anterior | Minutos si el artefacto sigue en el registry |
| Feature flag | Apagar flag | Segundos; ideal |
| Migración expand (ADD) | DROP columna nueva o ignorarla | Seguro si nullable y sin deps |
| Migración contract (DROP) | Restaurar backup / forward fix | Evita llegar aquí sin certeza |
| Migración destructiva de datos | Backup point-in-time | Ensayar restore en staging |
# 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).
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.mdcon versiones finales. - Node LTS alineada en local, CI, Docker y producción.
npm auditsin 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»).
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íntoma | Causa probable | Solución |
|---|---|---|
| Peer dependency warnings y runtime crash Nest | Majors @nestjs/* desalineadas | Instalar el conjunto completo de paquetes Nest a la misma major |
| Build Angular falla en librería UI | Lib aún ViewEngine / incompatible Ivy | Actualizar lib, sustituirla o aislarla |
| Tests HttpClient rompen tras update | Sigue HttpClientModule + interceptores DI | provideHttpClient + provideHttpClientTesting |
| UI no refresca tras quitar Zone | Callbacks externos sin signals | Envolver estado en signals o notificar CD explícita |
| Migración MikroORM hace DROP inesperado | Diff de entidad sin mapear fieldName | Revisar SQL; mapear columnas; expand/contract |
| Canary «verde» pero negocio caído | Métricas solo de infra | SLIs de crear/asignar tarea, login, checkout |
ng update deja el repo a medias | Conflictos / working tree sucio | Rama limpia, major a major, commit por salto |
| Docker prod con Node 16 y app en 20 | Inventario incompleto | Misma LTS en todos los entornos |
| Rollback imposible tras DROP COLUMN | Contract prematuro | No contraer hasta lecturas 100 % nuevas |
| Strict NullChecks tras update = 2000 errores | Se activó todo a la vez | Flags graduales + métrica de errores tsc |
{
"dependencies": {
"@nestjs/common": "10.3.0",
"@nestjs/core": "8.4.7",
"@nestjs/platform-express": "9.4.0"
}
}
{
"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:updateen producción. - Desactivar
strictpara 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
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).
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.
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.
Ejercicio 4. Explica con tus palabras la diferencia entre big bang, incremental y strangler
fig aplicados a la ruta /reports de TaskFlow.
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.
Ejercicio 6. Dado un TasksModule con tres componentes, describe el orden exacto
para pasarlos a standalone sin romper el lazy loading actual.
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.
Ejercicio 8. Redacta una regla no-restricted-imports que prohíba
HttpClientModule y toPromise, con mensajes orientados a TaskFlow.
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.
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.
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.
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:
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.
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
- Angular Update Guide (update.angular.dev) — checklist oficial origen/destino.
- Angular.dev · Keeping up-to-date — prácticas de actualización continua.
- Angular.dev · Components (standalone) — modelo standalone actual.
- Angular.dev · Signals — reactividad moderna.
- Angular.dev · Zoneless — change detection sin Zone.js (consulta tu versión).
- NestJS Migration Guide — breaking changes entre majors.
- NestJS Documentation — referencia de Application Context, providers y platforms.
- MikroORM · Upgrading from v5 to v6 — guía oficial 5→6.
- MikroORM · Migrations — CLI y flujo de migraciones.
- Node.js Release Schedule — ventanas LTS.
- Martin Fowler · Strangler Fig Application — patrón de estrangulamiento.
- Martin Fowler · Parallel Change — base conceptual de expand/contract.
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».