GUIDE

Node.js: Construyendo Microservicios Escalables

Guía técnica profunda sobre arquitectura Node.js, frameworks, patrones de concurrencia y optimización de rendimiento en producción. Basada en 26 microservicios construidos en producción con mensajería ZeroMQ y colas Redis.

Node.jsExpressFastifyNestJSTypeScriptZeroMQRedisDocker

Índice de Contenidos

  1. 1. El Event Loop e I/O No Bloqueante
  2. 2. Comparación de Frameworks: Express, Fastify, NestJS
  3. 3. ES Modules e Integración con TypeScript
  4. 4. Clustering y Worker Threads
  5. 5. Procesamiento de Streams
  6. 6. Patrones de Manejo de Errores
  7. 7. Optimización de Rendimiento
  8. 8. Mejores Prácticas de Seguridad (OWASP)
  9. 9. Gestión de Dependencias
  10. 10. Testing con Jest
  11. 11. Características Built-in de Node.js Moderno (22+ / 25+)

1. El Event Loop e I/O No Bloqueante

El event loop es el corazón de Node.js. Es un mecanismo de hilo único que orquesta operaciones asíncronas a través de una serie de fases: timers, pending callbacks, idle/prepare, poll, check y close callbacks. Entender estas fases es crítico para construir microservicios que manejen miles de conexiones concurrentes sin overhead de hilos.

Fases del Event Loop

Cada iteración (o "tick") del event loop procesa callbacks en un orden fijo. La fase poll es donde se ejecutan la mayoría de callbacks de I/O. setTimeout y setInterval se disparan en la fase de timers, mientras que setImmediate se ejecuta en la fase check. La cola de process.nextTick y la cola de microtareas (Promises) se drenan entre cada transición de fase, dándoles mayor prioridad que cualquier callback de fase.setTimeout and setInterval fire in the timers phase, while setImmediate executes in the check phase. The process.nextTick queue and microtask queue (Promises) are drained between every phase transition, giving them higher priority than any phase callback.

// Execution order demonstration
setTimeout(() => console.log('1: timer'), 0);
setImmediate(() => console.log('2: immediate'));
process.nextTick(() => console.log('3: nextTick'));
Promise.resolve().then(() => console.log('4: microtask'));

// Output: 3: nextTick, 4: microtask, 1: timer, 2: immediate
// nextTick and microtasks always run before I/O phases

Evitando el Bloqueo del Event Loop

Una sola operación intensiva de CPU bloquea todo el event loop. En microservicios en producción, esto se manifiesta como latencia incrementada en todos los endpoints. Los culpables comunes incluyen el parsing de JSON de payloads grandes, operaciones criptográficas síncronas y evaluación de regex complejas. El flag --max-old-space-size controla límites del heap pero no previene el bloqueo. En su lugar, delega computación pesada a worker threads o procesos hijo.--max-old-space-size flag controls heap limits but does not prevent blocking. Instead, offload heavy computation to worker threads or child processes.

En producción, monitoreábamos el lag del event loop usando monitorEventLoopDelay() de perf_hooks. Cualquier servicio que excediera 50ms de lag p99 disparaba una alerta, motivándonos a identificar y extraer operaciones bloqueantes a worker threads dedicados.monitorEventLoopDelay() from perf_hooks. Any service exceeding 50ms p99 lag triggered an alert, prompting us to identify and extract blocking operations into dedicated worker threads.

libuv y el Thread Pool

Node.js delega ciertas operaciones al thread pool de libuv (tamaño por defecto: 4). Las búsquedas DNS, operaciones del sistema de archivos y algunas funciones crypto usan este pool. Cuando el pool está saturado, las operaciones se encolan, causando latencia inesperada. Configura UV_THREADPOOL_SIZE (máx 1024) basado en tu carga de trabajo. Para microservicios con I/O pesado de archivos, incrementar el pool a 16-32 hilos puede reducir significativamente la latencia de cola.UV_THREADPOOL_SIZE (max 1024) based on your workload. For microservices performing heavy file I/O, increasing the pool size to 16-32 threads can significantly reduce tail latency.

2. Comparación de Frameworks: Express, Fastify, NestJS

Express

Express sigue siendo el framework Node.js más adoptado. Su pipeline de middleware es simple: cada middleware llama a next() para pasar el control. Sin embargo, Express no tiene validación de esquemas integrada, no tiene soporte nativo de TypeScript, y su router usa matching lineal, que se degrada a escala. Para microservicios que manejan menos de 10,000 req/s con ruteo simple, Express es adecuado. Más allá, considera Fastify.next() to pass control downstream. However, Express has no built-in schema validation, no native TypeScript support, and its router uses linear matching, which degrades at scale. For microservices handling fewer than 10,000 req/s with simple routing, Express is adequate. Beyond that, consider Fastify.

// Express middleware pipeline
app.use(helmet());
app.use(cors({ origin: config.allowedOrigins }));
app.use(express.json({ limit: '1mb' }));
app.use(requestId());        // attach X-Request-Id
app.use(requestLogger());    // structured logging
app.use('/api/v1', router);
app.use(errorHandler());     // centralized error handling

Fastify

Fastify logra 2-3x mayor throughput que Express usando un router de árbol radix, serialización basada en JSON schema (vía fast-json-stringify), y una arquitectura de plugins optimizada. Su modelo de encapsulación previene conflictos entre plugins y habilita composición modular genuina. La validación de schemas no es decoración opcional sino una característica de rendimiento de primera clase: Fastify compila JSON schemas en funciones optimizadas de validación y serialización al arrancar.fast-json-stringify), and an optimized plugin architecture. Its encapsulation model prevents plugin conflicts and enables genuine modular composition. Schema validation is not optional decoration but a first-class performance feature: Fastify compiles JSON schemas into optimized validation and serialization functions at startup.

// Fastify with schema-based validation and serialization
fastify.route({
  method: 'POST',
  url: '/api/users',
  schema: {
    body: {
      type: 'object',
      required: ['email', 'name'],
      properties: {
        email: { type: 'string', format: 'email' },
        name: { type: 'string', minLength: 2, maxLength: 100 }
      }
    },
    response: {
      201: {
        type: 'object',
        properties: {
          id: { type: 'string', format: 'uuid' },
          email: { type: 'string' },
          createdAt: { type: 'string', format: 'date-time' }
        }
      }
    }
  },
  handler: async (request, reply) => {
    const user = await userService.create(request.body);
    reply.code(201).send(user);
  }
});

NestJS

NestJS provee una arquitectura opinada inspirada en Angular: módulos, controladores, servicios, guards, interceptors, pipes y filtros de excepción. Usa decoradores e inyección de dependencias, haciéndolo ideal para equipos grandes y configuraciones monorepo. Internamente, NestJS puede usar Express o Fastify como adaptador HTTP. Para la plataforma, NestJS con adaptador Fastify nos dio consistencia arquitectónica y alto throughput.

// NestJS controller with decorators and DI
@Controller('subscriptions')
@UseGuards(JwtAuthGuard, RolesGuard)
export class SubscriptionController {
  constructor(
    private readonly subscriptionService: SubscriptionService,
    private readonly eventBus: EventBus,
  ) {}

  @Post()
  @Roles(Role.ADMIN)
  @HttpCode(HttpStatus.CREATED)
  async create(@Body() dto: CreateSubscriptionDto): Promise<Subscription> {
    const sub = await this.subscriptionService.create(dto);
    this.eventBus.publish(new SubscriptionCreatedEvent(sub));
    return sub;
  }
}
En producción, empezamos con Express pero migramos microservicios críticos (pagos, agendamiento, notificaciones) a NestJS con el adaptador Fastify. La migración mejoró el throughput de requests 2.4x en el servicio de agendamiento, que maneja 8,000+ reservas concurrentes de clases de gimnasio en horas pico. El sistema de módulos de NestJS nos permitió compartir pipes de validación, guards de autenticación e interceptors de logging entre los 16+ servicios a través de un paquete de librería compartida.

3. ES Modules e Integración con TypeScript

Node.js soporta ES Modules nativamente desde v12 (estable en v16+). Configura "type": "module" en package.json para tratar todos los archivos .js como ESM, o usa extensiones .mjs. La interop con CommonJS funciona a través de createRequire() o import() dinámico. Para nuevos microservicios, ESM es la elección correcta: habilita tree-shaking en bundlers, provee análisis estático y se alinea con el estándar de módulos del navegador."type": "module" in package.json to treat all .js files as ESM, or use .mjs extensions. CommonJS interop works through createRequire() or dynamic import(). For new microservices, ESM is the correct choice: it enables tree-shaking in bundlers, provides static analysis, and aligns with the browser module standard.

Estrategias de Compilación TypeScript

Para microservicios, elige entre tsc (implementación de referencia, verificación de tipos completa), esbuild (50-100x más rápido, sin verificación de tipos), o swc (basado en Rust, 20x más rápido). El patrón de producción: usa tsc --noEmit para verificación de tipos en CI, y esbuild o swc para builds rápidos. Con monorepos NestJS, las project references de tsc habilitan builds incrementales entre paquetes.tsc (reference implementation, full type checking), esbuild (50-100x faster, no type checking), or swc (Rust-based, 20x faster). The production pattern: use tsc --noEmit for type checking in CI, and esbuild or swc for fast builds. With NestJS monorepos, tsc project references enable incremental builds across packages.

// tsconfig.json for a Node.js microservice (ESM output)
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "incremental": true,
    "tsBuildInfoFile": "./dist/.tsbuildinfo"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.spec.ts"]
}

Al usar ESM con TypeScript, todas las importaciones relativas deben incluir la extensión .js (aunque los archivos fuente sean .ts). Esto es porque TypeScript no reescribe los especificadores de import. La configuración "moduleResolution": "NodeNext" refuerza este requisito a nivel de tipos..js extension (even though the source files are .ts). This is because TypeScript does not rewrite import specifiers. The "moduleResolution": "NodeNext" setting enforces this requirement at the type level.

En producción, migramos de CommonJS a ESM en 26 microservicios incrementalmente. La clave fue configurar "type": "module" por paquete y usar project references de tsc con esbuild para builds rápidos. CI ejecutaba tsc --noEmit en cada PR para detectar errores de tipos sin ralentizar el build. El tiempo total de build del monorepo completo bajó de 4 minutos a 35 segundos al cambiar de emisión tsc a esbuild."type": "module" per package and using tsc project references with esbuild for fast builds. CI ran tsc --noEmit on every PR to catch type errors without slowing the build. Total build time for the entire monorepo dropped from 4 minutes to 35 seconds after switching from tsc emit to esbuild.

4. Clustering y Worker Threads

El Módulo Cluster

El módulo cluster bifurca el proceso Node.js en múltiples workers compartiendo el mismo puerto del servidor. El proceso principal distribuye conexiones usando round-robin (por defecto en Linux) o balanceo de carga a nivel de SO. Cada worker ejecuta su propio event loop y aislamiento V8. Para microservicios HTTP, cluster.fork() multiplicado por cantidad de CPUs satura los cores disponibles. Sin embargo, este enfoque duplica el uso de memoria por worker y no provee estado compartido.cluster module forks the Node.js process into multiple workers sharing the same server port. The primary process distributes connections using round-robin (default on Linux) or OS-level load balancing. Each worker runs its own event loop and V8 isolate. For HTTP microservices, cluster.fork() multiplied by CPU count saturates available cores. However, this approach doubles memory usage per worker and provides no shared state.

import cluster from 'node:cluster';
import { availableParallelism } from 'node:os';

if (cluster.isPrimary) {
  const numWorkers = availableParallelism();
  console.log(`Primary ${process.pid}: forking ${numWorkers} workers`);

  for (let i = 0; i < numWorkers; i++) cluster.fork();

  cluster.on('exit', (worker, code) => {
    console.error(`Worker ${worker.process.pid} died (code ${code}). Restarting...`);
    cluster.fork(); // auto-restart crashed workers
  });
} else {
  // Each worker runs the full HTTP server
  await startServer();
}

Worker Threads para Tareas CPU-Bound

A diferencia del clustering (procesos separados), los worker threads comparten la misma memoria del proceso vía SharedArrayBuffer y Atomics. Usa worker threads para operaciones CPU-intensivas: procesamiento de imágenes, generación de PDFs, hashing criptográfico o transformaciones de datos complejas. La comunicación ocurre a través de MessagePort usando el algoritmo de structured clone. Transfiere ArrayBuffers en vez de copiarlos para rendimiento zero-copy.SharedArrayBuffer and Atomics. Use worker threads for CPU-intensive operations: image processing, PDF generation, cryptographic hashing, or complex data transformations. Communication happens through MessagePort using the structured clone algorithm. Transfer ArrayBuffers instead of copying them for zero-copy performance.

// worker-pool.ts - reusable worker thread pool
import { Worker } from 'node:worker_threads';
import { EventEmitter } from 'node:events';

class WorkerPool extends EventEmitter {
  private workers: Worker[] = [];
  private queue: Array<{ data: any; resolve: Function; reject: Function }> = [];
  private freeWorkers: Worker[] = [];

  constructor(private script: string, private size: number) {
    super();
    for (let i = 0; i < size; i++) this.addWorker();
  }

  private addWorker() {
    const w = new Worker(this.script);
    w.on('message', (result) => {
      w._currentTask?.resolve(result);
      w._currentTask = null;
      this.freeWorkers.push(w);
      this.drain();
    });
    this.freeWorkers.push(w);
  }

  async run(data: any): Promise<any> {
    return new Promise((resolve, reject) => {
      this.queue.push({ data, resolve, reject });
      this.drain();
    });
  }

  private drain() {
    while (this.queue.length && this.freeWorkers.length) {
      const w = this.freeWorkers.pop()!;
      const task = this.queue.shift()!;
      w._currentTask = task;
      w.postMessage(task.data);
    }
  }
}
En producción, nuestro servicio de generación de facturas PDF inicialmente bloqueaba el event loop por 200-800ms por documento. Moverlo a un pool de worker threads de 4 workers redujo la latencia p99 en los endpoints HTTP principales de 1200ms a 45ms manteniendo el mismo throughput para generación de facturas.

5. Procesamiento de Streams

Los streams son la abstracción más poderosa de Node.js para manejar grandes datasets sin cargar todo en memoria. Los cuatro tipos de stream (Readable, Writable, Duplex, Transform) implementan backpressure automáticamente. Cuando el buffer interno de un writable stream excede highWaterMark, señaliza al readable stream que pause, previniendo el agotamiento de memoria.highWaterMark, it signals the readable stream to pause, preventing memory exhaustion.

La Función pipeline()

Siempre usa pipeline() de node:stream/promises en lugar de .pipe(). La función pipeline maneja propagación de errores, limpieza de streams y backpressure correctamente. Un pipe roto en medio de una cadena destruye apropiadamente todos los streams y libera recursos. Con .pipe(), errores en streams intermedios pueden dejar recursos colgados.pipeline() from node:stream/promises instead of .pipe(). The pipeline function handles error propagation, stream cleanup, and backpressure correctly. A broken pipe in the middle of a chain properly destroys all streams and frees resources. With .pipe(), errors on intermediate streams can leave resources dangling.

import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';
import { Transform } from 'node:stream';

// CSV processing pipeline: read -> parse -> transform -> compress -> write
const csvParser = new Transform({
  objectMode: true,
  transform(chunk, encoding, callback) {
    const lines = chunk.toString().split('\n').filter(Boolean);
    for (const line of lines) {
      const [id, name, amount] = line.split(',');
      this.push(JSON.stringify({ id, name, amount: parseFloat(amount) }) + '\n');
    }
    callback();
  }
});

await pipeline(
  createReadStream('./transactions.csv', { highWaterMark: 64 * 1024 }),
  csvParser,
  createGzip(),
  createWriteStream('./transactions.json.gz')
);

Async Iterators con Streams

Desde Node.js v10, los readable streams implementan el protocolo async iterable. Esto permite procesar streams con loops for await...of, que es más limpio que event listeners para procesamiento secuencial. Combinado con Readable.from(), puedes crear streams desde cualquier async iterable, conectando los modelos de datos pull-based y push-based.for await...of loops, which is cleaner than event listeners for sequential processing. Combined with Readable.from(), you can create streams from any async iterable, bridging the gap between pull-based and push-based data models.

import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';

// Process a large log file line by line with constant memory usage
const rl = createInterface({
  input: createReadStream('/var/log/app/access.log'),
  crlfDelay: Infinity
});

const errorCounts = new Map<string, number>();
for await (const line of rl) {
  const match = line.match(/HTTP\/\d\.\d" (\d{3})/);
  if (match && parseInt(match[1]) >= 500) {
    const code = match[1];
    errorCounts.set(code, (errorCounts.get(code) || 0) + 1);
  }
}
En producción, el microservicio de reportes usaba streams para generar exportaciones CSV de datos de membresía (500,000+ filas). Usando pipeline() con un Transform stream y createGzip(), el servicio transmitía datos comprimidos directamente a la respuesta HTTP. El uso de memoria se mantuvo bajo 50MB independientemente del tamaño del dataset, comparado con 1.2GB al cargar todo en memoria con JSON.stringify.pipeline() with a Transform stream and createGzip(), the service streamed compressed data directly to the HTTP response. Memory usage stayed under 50MB regardless of dataset size, compared to 1.2GB when loading everything into memory with JSON.stringify.

6. Patrones de Manejo de Errores

El manejo de errores en microservicios Node.js requiere una estrategia en capas: errores operacionales (fallos esperados como timeouts de red o errores de validación) deben ser capturados y manejados graciosamente, mientras que errores de programador (bugs como referencia nula o incompatibilidad de tipos) deben crashear el proceso y dejar que el orquestador lo reinicie.

Jerarquía de Errores Personalizados

// errors/base.ts
export abstract class AppError extends Error {
  abstract readonly statusCode: number;
  abstract readonly isOperational: boolean;

  constructor(message: string, public readonly context?: Record<string, unknown>) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

export class NotFoundError extends AppError {
  readonly statusCode = 404;
  readonly isOperational = true;
}

export class ValidationError extends AppError {
  readonly statusCode = 400;
  readonly isOperational = true;
  constructor(message: string, public readonly fields: Record<string, string>) {
    super(message, { fields });
  }
}

export class ExternalServiceError extends AppError {
  readonly statusCode = 502;
  readonly isOperational = true;
  constructor(service: string, cause: Error) {
    super(`External service failure: ${service}`, { service, cause: cause.message });
  }
}

Límites de Error Globales

Cada microservicio debe registrar handlers para uncaughtException, unhandledRejection y eventos de señal. Ante una excepción no capturada, registra el error, vacía la telemetría, cierra conexiones de base de datos graciosamente y sal con código 1. Kubernetes o PM2 reinicia el proceso. Nunca intentes continuar ejecutando después de una excepción no capturada; el estado del proceso no es confiable.uncaughtException, unhandledRejection, and signal events. On an uncaught exception, log the error, flush telemetry, close database connections gracefully, and exit with code 1. Kubernetes or PM2 restarts the process. Never attempt to continue running after an uncaught exception; the process state is unreliable.

// Graceful shutdown handler
const shutdown = async (signal: string) => {
  logger.info({ signal }, 'Shutdown signal received');
  server.close();                      // stop accepting connections
  await db.end();                      // close database pool
  await redis.quit();                  // close Redis connection
  await telemetry.flush();             // flush traces/metrics
  process.exit(0);
};

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

process.on('uncaughtException', (err) => {
  logger.fatal({ err }, 'Uncaught exception - shutting down');
  shutdown('uncaughtException').finally(() => process.exit(1));
});

process.on('unhandledRejection', (reason) => {
  logger.fatal({ err: reason }, 'Unhandled rejection - shutting down');
  shutdown('unhandledRejection').finally(() => process.exit(1));
});
En producción, cada microservicio usaba la misma jerarquía AppError compartida vía el paquete común del monorepo. El manejador de errores centralizado mapeaba errores isOperational a respuestas JSON estructuradas con IDs de correlación, mientras que errores de programador disparaban alertas de PagerDuty. Este patrón detectó un leak crítico de conexión Redis en el servicio de agendamiento en minutos: la tasa de ExternalServiceError se disparó, la alerta se activó y identificamos una llamada .quit() faltante en un job de background.AppError hierarchy shared via the monorepo's common package. The centralized error handler mapped isOperational errors to structured JSON responses with correlation IDs, while programmer errors triggered PagerDuty alerts. This pattern caught a critical Redis connection leak in the scheduling service within minutes: the ExternalServiceError rate spiked, the alert fired, and we identified a missing .quit() call in a background job.

7. Optimización de Rendimiento

Profiling con V8 Inspector

Inicia tu servicio con --inspect y conecta Chrome DevTools para perfilar CPU y memoria. Para profiling en producción sin DevTools, usa los flags --cpu-prof y --heap-prof para generar archivos de perfil V8, luego analízalos con speedscope o 0x. La suite clinic.js (Doctor, Bubbleprof, Flame) automatiza la detección de cuellos de botella.--inspect and connect Chrome DevTools to profile CPU and memory. For production profiling without DevTools, use --cpu-prof and --heap-prof flags to generate V8 profile files, then analyze them with speedscope or 0x. The clinic.js suite (Doctor, Bubbleprof, Flame) automates bottleneck detection.

Detectando Memory Leaks

Los memory leaks en microservicios de larga ejecución típicamente provienen de: cachés sin límite (usa LRU con tamaño máximo), acumulación de event listeners (siempre llama removeListener), closures que capturan scopes grandes, y arrays globales que crecen con el tiempo. Monitorea process.memoryUsage() y configura heap snapshot diffing en staging para detectar leaks antes de producción.removeListener), closures capturing large scopes, and global arrays that grow over time. Monitor process.memoryUsage() and set up heap snapshot diffing in staging to catch leaks before production.

// Periodic memory monitoring
setInterval(() => {
  const { heapUsed, heapTotal, rss, external } = process.memoryUsage();
  metrics.gauge('nodejs.heap_used', heapUsed);
  metrics.gauge('nodejs.heap_total', heapTotal);
  metrics.gauge('nodejs.rss', rss);
  metrics.gauge('nodejs.external', external);

  // Alert if heap usage exceeds 85% of total
  if (heapUsed / heapTotal > 0.85) {
    logger.warn({ heapUsed, heapTotal }, 'High heap utilization');
  }
}, 30_000);

Técnicas Clave de Optimización

En producción, perfilando el microservicio de pagos con clinic flame revelamos que JSON.stringify en objetos grandes de suscripción consumía 18% del tiempo de CPU. Cambiar a fast-json-stringify con schemas precompilados redujo el tiempo de serialización 12x y recortó el tiempo de respuesta p95 de 120ms a 34ms. También incrementamos UV_THREADPOOL_SIZE a 16 en el servicio de exportación de archivos, lo que eliminó un misterioso pico de latencia de 2 segundos que ocurría cuando 4+ exportaciones CSV concurrentes saturaban el thread pool por defecto.clinic flame revealed that JSON.stringify on large subscription objects consumed 18% of CPU time. Switching to fast-json-stringify with precompiled schemas reduced serialization time by 12x and cut p95 response time from 120ms to 34ms. We also increased UV_THREADPOOL_SIZE to 16 on the file-export service, which eliminated a mysterious 2-second latency spike that occurred when 4+ concurrent CSV exports saturated the default thread pool.

8. Mejores Prácticas de Seguridad (OWASP)

Los microservicios Node.js están expuestos a los mismos vectores de ataque que cualquier servicio HTTP, más riesgos específicos del lenguaje como prototype pollution y ReDoS. El OWASP Top 10 y la Guía de Seguridad de OWASP para Node.js proveen la base para un enfoque de defensa en profundidad que aborda cada capa.

OWASP Top 10 en el Contexto de Node.js

El OWASP Top 10 se mapea directamente a patrones de Node.js: A01 (Control de Acceso Roto) requiere validación adecuada de JWT y guards de roles en cada ruta; A02 (Fallos Criptográficos) significa usar crypto.timingSafeEqual para comparaciones y evitar algoritmos deprecados; A03 (Inyección) demanda queries parametrizados (nunca concatenación de strings para SQL/NoSQL); A04 (Diseño Inseguro) se mitiga modelando amenazas en cada límite de microservicio; A05 (Configuración Incorrecta de Seguridad) se previene con valores por defecto de Helmet y políticas CORS estrictas.crypto.timingSafeEqual for comparisons and avoiding deprecated algorithms; A03 (Injection) demands parameterized queries (never string concatenation for SQL/NoSQL); A04 (Insecure Design) is mitigated by threat modeling each microservice boundary; A05 (Security Misconfiguration) is prevented with Helmet defaults and strict CORS policies.

En producción, cada microservicio pasaba por un checklist de seguridad alineado con las directrices de OWASP antes del despliegue: auditoría de dependencias con cero vulnerabilidades críticas/altas, headers Helmet habilitados, rate limiting configurado, schemas de entrada validados y secretos almacenados en Kubernetes sealed secrets. Esto previno cualquier incidente de seguridad en 16+ servicios durante 3 años de operación.

9. Gestión de Dependencias

En una arquitectura de microservicios, la gestión de dependencias es una preocupación operacional crítica. Cada servicio lleva sus propios node_modules, y una sola dependencia transitiva vulnerable u obsoleta puede comprometer todo el sistema. Un enfoque disciplinado de higiene de dependencias previene ataques de supply chain, reduce el tamaño de imágenes de contenedores y asegura builds reproducibles.node_modules, and a single vulnerable or outdated transitive dependency can compromise the entire system. A disciplined approach to dependency hygiene prevents supply chain attacks, reduces container image sizes, and ensures reproducible builds.

Lockfiles e Instalaciones Deterministas

Siempre haz commit de package-lock.json (npm) o pnpm-lock.yaml (pnpm) al control de versiones. Usa npm ci (no npm install) en pipelines de CI/CD para instalaciones deterministas y reproducibles. El comando ci elimina node_modules antes de instalar, asegurando que el lockfile sea la única fuente de verdad. Para monorepos, el almacenamiento content-addressable de pnpm deduplica paquetes entre workspaces, reduciendo el uso de disco 60-80%.package-lock.json (npm) or pnpm-lock.yaml (pnpm) to version control. Use npm ci (not npm install) in CI/CD pipelines for deterministic, reproducible installs. The ci command removes node_modules before installing, ensuring the lockfile is the single source of truth. For monorepos, pnpm's content-addressable store deduplicates packages across workspaces, reducing disk usage by 60-80%.

# CI pipeline dependency install
npm ci --ignore-scripts        # skip postinstall scripts (security)
npm audit --audit-level=high   # fail on high/critical vulnerabilities
npx --yes license-checker-webpack-plugin --failOnUnlicensed  # license compliance

# For pnpm monorepos
pnpm install --frozen-lockfile
pnpm audit --audit-level high
pnpm -r exec -- npx depcheck   # find unused dependencies per workspace

Actualizaciones Automatizadas y Escaneo de Vulnerabilidades

Usa Dependabot o Renovate para automatizar actualizaciones de dependencias. Configura reglas de agrupamiento para agrupar actualizaciones minor/patch semanalmente y manejar actualizaciones major individualmente. Integra npm audit o snyk test en el pipeline de CI para bloquear merges con vulnerabilidades conocidas. Para contenedores de producción, usa builds Docker multi-stage e instala solo dependencias de producción (npm ci --omit=dev) para minimizar la superficie de ataque.npm audit or snyk test into the CI pipeline to block merges with known vulnerabilities. For production containers, use multi-stage Docker builds and install only production dependencies (npm ci --omit=dev) to minimize the attack surface.

// .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      minor-and-patch:
        update-types: ["minor", "patch"]
    open-pull-requests-limit: 10
    reviewers: ["josenobile"]
    labels: ["dependencies"]

Estrategia de Dependencias en Monorepo

En un monorepo con múltiples microservicios, aplica una política de versión única para dependencias compartidas (TypeScript, ESLint, Prettier, librerías de testing) para prevenir conflictos de versiones. Usa protocolos de workspace ("workspace:*" en pnpm) para paquetes internos. La herramienta syncpack valida consistencia de versiones en todos los archivos package.json del monorepo."workspace:*" in pnpm) for internal packages. The syncpack tool validates version consistency across all package.json files in the monorepo.

En producción, usamos pnpm workspaces para 26 microservicios. Renovate abría PRs agrupados para actualizaciones minor semanalmente, y CI bloqueaba cualquier PR con hallazgos high/critical de npm audit. Manteníamos paquetes compartidos @myorg/eslint-config y @myorg/tsconfig para enforcer tooling consistente. Cuando el ataque de supply chain de event-stream golpeó el ecosistema npm, nuestra política de --ignore-scripts y lockfiles fijados significaron cero impacto en todos los servicios.npm audit high/critical findings. We maintained a shared @myorg/eslint-config and @myorg/tsconfig package to enforce consistent tooling. When the event-stream supply chain attack hit the npm ecosystem, our --ignore-scripts policy and pinned lockfiles meant zero impact across all services.

10. Testing con Jest

Testear microservicios requiere un enfoque en capas: tests unitarios validan funciones y clases individuales, tests de integración verifican interacciones entre componentes (base de datos, cola de mensajes, APIs externas), y tests end-to-end confirman el ciclo completo de request-response. Jest es el framework de testing estándar para Node.js y TypeScript, ofreciendo mocking integrado, cobertura de código, snapshot testing y ejecución paralela de tests.

Testing Unitario con Jest y TypeScript

Configura Jest con ts-jest o @swc/jest para soporte TypeScript. Usa @swc/jest para ejecución de tests más rápida (10-20x más rápido que ts-jest). Organiza tests junto a archivos fuente (*.spec.ts) o en un directorio paralelo __tests__. Simula dependencias externas usando jest.mock() e inyecta test doubles a través de inyección por constructor (el DI de NestJS hace esto trivial).ts-jest or @swc/jest for TypeScript support. Use @swc/jest for faster test execution (10-20x faster than ts-jest). Organize tests next to source files (*.spec.ts) or in a parallel __tests__ directory. Mock external dependencies using jest.mock() and inject test doubles through constructor injection (NestJS's DI makes this trivial).

// subscription.service.spec.ts
import { Test } from '@nestjs/testing';
import { SubscriptionService } from './subscription.service';
import { SubscriptionRepository } from './subscription.repository';
import { EventBus } from '../events/event-bus';

describe('SubscriptionService', () => {
  let service: SubscriptionService;
  let repo: jest.Mocked<SubscriptionRepository>;
  let eventBus: jest.Mocked<EventBus>;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        SubscriptionService,
        { provide: SubscriptionRepository, useValue: { create: jest.fn(), findById: jest.fn() } },
        { provide: EventBus, useValue: { publish: jest.fn() } },
      ],
    }).compile();

    service = module.get(SubscriptionService);
    repo = module.get(SubscriptionRepository);
    eventBus = module.get(EventBus);
  });

  it('should create a subscription and publish an event', async () => {
    const dto = { userId: 'u1', planId: 'plan-monthly', gymId: 'gym-123' };
    const expected = { id: 'sub-1', ...dto, status: 'active', createdAt: new Date() };
    repo.create.mockResolvedValue(expected);

    const result = await service.create(dto);

    expect(result).toEqual(expected);
    expect(repo.create).toHaveBeenCalledWith(dto);
    expect(eventBus.publish).toHaveBeenCalledWith(
      expect.objectContaining({ subscriptionId: 'sub-1' })
    );
  });

  it('should throw NotFoundError for non-existent subscription', async () => {
    repo.findById.mockResolvedValue(null);
    await expect(service.findById('invalid')).rejects.toThrow('Subscription not found');
  });
});

Testing de Integración

Los tests de integración verifican que los componentes del microservicio funcionen correctamente juntos con bases de datos y colas de mensajes reales. Usa testcontainers para levantar contenedores Docker efímeros (MySQL, Redis, RabbitMQ) para cada suite de tests. El Test.createTestingModule() de NestJS arranca el contenedor DI completo, habilitando tests realistas sin cableado manual. Ejecuta tests de integración en un proyecto Jest separado con timeouts más largos.testcontainers to spin up ephemeral Docker containers (MySQL, Redis, RabbitMQ) for each test suite. NestJS's Test.createTestingModule() bootstraps the full DI container, enabling realistic tests without manual wiring. Run integration tests in a separate Jest project with longer timeouts.

// jest.config.ts - multi-project configuration
export default {
  projects: [
    {
      displayName: 'unit',
      testMatch: ['<rootDir>/src/**/*.spec.ts'],
      transform: { '^.+\\.tsx?$': ['@swc/jest'] },
      moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1' },
    },
    {
      displayName: 'integration',
      testMatch: ['<rootDir>/test/**/*.integration.ts'],
      transform: { '^.+\\.tsx?$': ['@swc/jest'] },
      testTimeout: 30_000,
      globalSetup: '<rootDir>/test/setup.ts',
      globalTeardown: '<rootDir>/test/teardown.ts',
    },
  ],
  collectCoverageFrom: ['src/**/*.ts', '!src/**/*.spec.ts', '!src/**/index.ts'],
  coverageThreshold: {
    global: { branches: 80, functions: 85, lines: 85, statements: 85 },
  },
};

Patrones de Testing para Microservicios

En producción, enforzábamos 85% de cobertura de código en los 26 microservicios vía gates de CI. Cada servicio tenía ~200-400 tests unitarios ejecutándose en menos de 10 segundos (usando @swc/jest) y 30-50 tests de integración usando testcontainers con MySQL y Redis. Los tests de contrato entre el servicio de pagos y el servicio de suscripciones detectaron 3 cambios breaking de API antes de llegar a staging. La suite total de tests del monorepo (4,000+ tests) se ejecutaba en paralelo en 90 segundos en CI.@swc/jest) and 30-50 integration tests using testcontainers with MySQL and Redis. Contract tests between the payments service and the subscription service caught 3 breaking API changes before they reached staging. The total test suite for the monorepo (4,000+ tests) ran in parallel in 90 seconds on CI.

11. Características Built-in de Node.js Moderno (22+ / 25+)

Node.js 22 a 26 incluyen características built-in poderosas que eliminan la necesidad de muchas dependencias de terceros. Estas adiciones hacen de Node.js una plataforma más autocontenida para construir servicios de producción.

Test Runner Built-in (node:test)

El módulo node:test es estable desde Node.js 22. Provee una alternativa a Jest sin dependencias con test runner integrado, reporters personalizados, cobertura de código vía --experimental-test-coverage, mock timers y el flag --test para auto-descubrimiento de archivos de test. Para microservicios que quieren minimizar dependencias, esta es ahora una opción viable para producción.node:test module is stable since Node.js 22. It provides a zero-dependency alternative to Jest with built-in test runner, custom reporters, code coverage via --experimental-test-coverage, mock timers, and the --test flag for auto-discovering test files. For microservices that want to minimize dependencies, this is now a viable production choice.

// test/user.test.ts - using built-in node:test
import { describe, it, mock, beforeEach } from 'node:test';
import assert from 'node:assert/strict';

describe('UserService', () => {
  beforeEach(() => mock.restoreAll());

  it('creates a user with valid email', async () => {
    const mockDb = mock.fn(async () => ({ id: '1', email: '[email protected]' }));
    const service = new UserService({ query: mockDb });
    const user = await service.create({ email: '[email protected]' });
    assert.equal(user.email, '[email protected]');
    assert.equal(mockDb.mock.callCount(), 1);
  });

  it('uses mock timers for TTL logic', async (t) => {
    t.mock.timers.enable({ apis: ['setTimeout'] });
    const cache = new TTLCache(60_000);
    cache.set('key', 'value');
    t.mock.timers.tick(61_000);
    assert.equal(cache.get('key'), undefined);
  });
});

// Run: node --test --test-reporter spec
// Coverage: node --test --experimental-test-coverage

Ejecución Nativa de TypeScript

Node.js 25.2+ tiene type stripping estable, permitiéndote ejecutar archivos .ts directamente con node file.ts. Sin paso de build, sin ts-node, sin tsx. Node.js elimina las anotaciones de tipo al cargar y ejecuta el JavaScript resultante. Para flujos de desarrollo y scripts, esto elimina completamente el paso de compilación. Nota: solo realiza type stripping -- no verifica tipos. Usa tsc --noEmit en CI para seguridad de tipos..ts files directly with node file.ts. No build step, no ts-node, no tsx needed. Node.js strips the type annotations at load time and executes the resulting JavaScript. For development workflows and scripts, this eliminates the compilation step entirely. Note: this performs type stripping only — it does not type-check. Use tsc --noEmit in CI for type safety.

// greeting.ts - runs directly with: node greeting.ts
interface User { name: string; role: 'admin' | 'user'; }

function greet(user: User): string {
  return `Hello, ${user.name} (${user.role})`;
}

console.log(greet({ name: 'Jose', role: 'admin' }));

SQLite Built-in (node:sqlite)

Node.js 22+ incluye un módulo experimental built-in node:sqlite para operaciones de base de datos embebida. Sin necesidad de instalar better-sqlite3 ni compilar addons nativos. Útil para caché local, almacén de configuración embebido, herramientas CLI y fixtures de tests.node:sqlite module for embedded database operations. No need to install better-sqlite3 or compile native addons. Useful for local caching, embedded configuration stores, CLI tools, and test fixtures.

import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE metrics (id INTEGER PRIMARY KEY, name TEXT, value REAL)');
const insert = db.prepare('INSERT INTO metrics (name, value) VALUES (?, ?)');
insert.run('cpu_usage', 45.2);
insert.run('memory_mb', 512.0);

const rows = db.prepare('SELECT * FROM metrics WHERE value > ?').all(40);
console.log(rows); // [{ id: 1, name: 'cpu_usage', value: 45.2 }, ...]

Modelo de Permisos

Node.js 22+ introduce un modelo de permisos para restringir acceso al sistema de archivos, red y procesos hijo en runtime. Usa --permission para habilitar el modelo, luego otorga capacidades específicas con --allow-fs-read, --allow-fs-write y --allow-child-process. Esta es una capa de defensa en profundidad para sandboxing de código no confiable o limitar el radio de impacto en producción.--permission to enable the model, then grant specific capabilities with --allow-fs-read, --allow-fs-write, and --allow-child-process. This is a defense-in-depth layer for sandboxing untrusted code or limiting blast radius in production.

// Run with restricted permissions:
// node --permission --allow-fs-read=/app/config --allow-fs-write=/app/logs app.js

// Attempting to read outside allowed paths throws ERR_ACCESS_DENIED
// Attempting to spawn child processes throws ERR_ACCESS_DENIED
// Attempting to use network without --allow-net throws ERR_ACCESS_DENIED

Modo Watch

node --watch is stable since Node.js 22. It automatically restarts the process when imported files change, eliminating the need for nodemon in development. Combine with --watch-path to restrict which directories are monitored.

# Development with built-in watch mode (no nodemon needed)
node --watch src/server.ts

# Watch specific paths only
node --watch-path=./src --watch-path=./config src/server.ts

Cliente WebSocket Built-in

Node.js 22+ habilita el global WebSocket built-in por defecto (basado en la implementación de undici). Sin necesidad de instalar el paquete ws para conexiones WebSocket del lado cliente. La API coincide con el estándar WebSocket del navegador.WebSocket global by default (based on the undici implementation). No need to install the ws package for client-side WebSocket connections. The API matches the browser WebSocket standard.

// Built-in WebSocket client (Node.js 22+, no ws package needed)
const ws = new WebSocket('wss://api.example.com/stream');

ws.addEventListener('open', () => {
  ws.send(JSON.stringify({ subscribe: 'metrics' }));
});

ws.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  console.log('Metric:', data);
});
Estas características built-in reducen significativamente la huella de dependencias de proyectos Node.js. En un nuevo microservicio, reemplazar Jest con node:test, nodemon con --watch, y el paquete ws con el WebSocket built-in elimina tres dependencias y sus árboles transitivos. Combinado con ejecución nativa de TypeScript para scripts de desarrollo, la cadena de herramientas se vuelve más liviana y rápida de configurar.node:test, nodemon with --watch, and the ws package with the built-in WebSocket eliminates three dependencies and their transitive trees. Combined with native TypeScript execution for development scripts, the toolchain becomes leaner and faster to set up.

Últimas Actualizaciones (Julio 2026)

Node.js 24 LTS: El Estándar de Producción Actual

Node.js 24.18.0 LTS es la versión recomendada actual, con soporte hasta el 30 de abril de 2028. Las adiciones clave incluyen el Permission Model estable (simplificado de --experimental-permission a --permission), ejecución nativa de TypeScript vía --strip-types habilitado por defecto para archivos .ts (sin necesidad de ts-node o paso de build), Undici 7 como cliente HTTP built-in con soporte de protocolos mejorado y mejor rendimiento, y V8 13.6 trayendo Float16Array y RegExp.escape(). El test runner built-in ahora espera automáticamente a que los subtests terminen, eliminando una fuente común de tests inestables.--experimental-permission to --permission), native TypeScript execution via --strip-types enabled by default for .ts files (no ts-node or build step needed), Undici 7 as the built-in HTTP client with improved protocol support and performance, and V8 13.6 bringing Float16Array and RegExp.escape(). The built-in test runner now automatically waits for subtests to finish, eliminating a common source of flaky tests.

Node.js 24: URLPattern, AsyncLocalStorage y Test Runner

Node.js 24 expone la API URLPattern en el objeto global, habilitando routing y matching declarativo de URLs sin librerías externas ni regex complejas. AsyncLocalStorage ahora usa AsyncContextFrame por defecto, proporcionando tracking de contexto asíncrono más eficiente para datos de scope de request como sesiones de usuario e IDs de tracing. El test runner built-in soporta ejecución paralela por defecto en sistemas multi-core y auto-espera subtests, y V8 13.6 trae Float16Array, RegExp.escape() y Atomics.pause(). Undici 7 ofrece rendimiento HTTP más rápido con soporte mejorado de HTTP/2 y HTTP/3.URLPattern API on the global object, enabling declarative URL routing and matching without external libraries or complex regex. AsyncLocalStorage now defaults to AsyncContextFrame, providing more efficient asynchronous context tracking for request-scoped data like user sessions and tracing IDs. The built-in test runner supports parallel execution by default on multi-core systems and auto-awaits subtests, and V8 13.6 brings Float16Array, RegExp.escape(), and Atomics.pause(). Undici 7 ships faster HTTP performance with improved HTTP/2 and HTTP/3 support.

Node.js 26: El Nuevo Release Current (Mayo 2026)

Node.js 26.0.0 se lanzó el 5 de mayo de 2026 como el nuevo release Current. Actualiza V8 a 14.6, habilita la API de fecha/hora Temporal por defecto y elimina varias APIs legacy que llevaban deprecadas múltiples versiones mayores. Node 26 entra en Active LTS el 28 de octubre de 2026 -- hasta entonces, Node 24 LTS sigue siendo el estándar de producción. Dos notas de calendario: Node.js 25 (impar) alcanzó su fin de vida el 1 de junio de 2026, y Node 26 es la última línea de releases bajo el modelo actual -- a partir de Node 27, el proyecto adopta una nueva estrategia de releases.Temporal date/time API by default, and removes several legacy APIs that had been deprecated across multiple major versions. Node 26 enters Active LTS on October 28, 2026 — until then, Node 24 LTS remains the production standard. Two scheduling notes: odd-numbered Node.js 25 reached end-of-life on June 1, 2026, and Node 26 is the last release line under the current release model — starting with Node 27, the project adopts a new release strategy.

Node.js 20 Fin de Vida: 30 de Abril de 2026

Node.js 20 reached end-of-life on April 30, 2026. Since that date, Node 20 receives no security patches or bug fixes. Teams still on Node 20 are running unsupported software and must upgrade to Node.js 24 LTS immediately. The upgrade path is straightforward: npm v11 (bundled with Node 24) is 65% faster than npm v9, and most applications require only dependency updates. Test with node --check and review the V8 breaking changes list for any deprecated APIs.

Node 20 alcanzó su EOL el 30 de abril de 2026 -- si aún lo ejecutas, actualizar a Node 24 LTS es el ítem de acción más urgente. Los beneficios son significativos: la ejecución nativa de TypeScript elimina la dependencia de ts-node, el Permission Model estable agrega seguridad en profundidad, y npm v11 acelera drásticamente las instalaciones. Para proyectos nuevos, Node 24 LTS con el test runner built-in, WebSocket built-in y ejecución nativa de .ts provee una cadena de herramientas drásticamente más liviana.

Más Guías