// 01 — INTRO

¿Qué es Node.js?

$ node --version → v20.x.x
Runtime de JavaScript fuera del navegador. Construido sobre V8 (motor de Chrome) + libuv para I/O asíncrono no bloqueante.

Node.js no es un lenguaje ni un framework — es un entorno de ejecución que permite correr JavaScript en el servidor, en la línea de comandos y en servicios como los del ERP. La clave es su modelo de I/O no bloqueante: mientras espera que la base de datos responda, atiende otras peticiones. Un solo proceso Node puede manejar miles de conexiones simultáneas.

🔄
Non-Blocking I/O
Operaciones de disco/red no bloquean el hilo. El Event Loop distribuye callbacks cuando están listos.
📦
NPM Ecosystem
2M+ paquetes públicos. Express, Moleculer, BullMQ, Prisma, Jest — instalables con un comando.
V8 + libuv
V8 compila JS a código máquina JIT. libuv abstrae I/O asíncrono del OS multiplataforma.
🧵
Single Thread
Un hilo JS principal, Worker Threads para CPU-bound. I/O se delega al thread pool de libuv.
bash — primeros pasos
$ node --version
v20.11.0
$ node -e "console.log('Hola desde Node!')"
Hola desde Node!
$ node # REPL interactivo
> process.version
'v20.11.0'
> process.platform
'linux'

// 02 — EVENT LOOP

El Event Loop

Mecanismo que permite a Node ser asíncrono con un solo hilo. Procesa callbacks en fases ordenadas: timers → I/O → poll → check → close.

Cuando Node encuentra operaciones asíncronas (leer archivo, consultar BD, esperar timer), las delega al sistema operativo y continúa ejecutando código. Cuando la operación termina, el callback queda en cola para la fase correcta del Event Loop.

📜CALL STACKcódigo síncrono
⏱️TIMERSsetTimeout/Interval
🌐I/O CALLBACKSfs, net, http
📊POLLnuevos eventos I/O
CHECKsetImmediate
microtasks (Promise.then / queueMicrotask) → se ejecutan entre CADA fase
event-loop-orden.js — orden de ejecuciónCORE
console.log('1 — síncrono');

setTimeout(() => console.log('4 — timer (macrotask)'), 0);

Promise.resolve().then(() => console.log('3 — microtask (Promise.then)'));

queueMicrotask(() => console.log('3b — microtask (queueMicrotask)'));

setImmediate(() => console.log('5 — setImmediate (check phase)'));

console.log('2 — síncrono, fin de script');

// Orden: 1 → 2 → 3 → 3b → 4 → 5
// Regla: Síncrono → Microtasks → Timers → setImmediate
Simulador del Event Loop — observa las fases en acción▶ LIVE
📜Call Stack
Microtasks
⏱️Timers
🌐I/O cb
setImmediate
Haz clic en Simular para ver el orden real del Event Loop…
⚠️
Nunca bloquees el Event LoopCódigo síncrono pesado (bucle enorme, JSON.parse de 50MB, bcrypt cost=20) bloquea el hilo principal. La solución: Worker Threads para CPU-intensivo, algoritmos más eficientes, o procesar en chunks pequeños.

// 03 — MÓDULOS

Módulos ESM y NPM

Node soporta CommonJS (require) y ES Modules (import/export). En proyectos nuevos: siempre ESM — es el estándar, soporta top-level await y tiene mejor tree-shaking.

package.json — habilitar ESM + scripts esencialesNPM
{
  "name": "erp-backend",
  "version": "1.0.0",
  "type": "module",        ← habilita ESM en todos los .js
  "engines": { "node": ">=20.0.0" },
  "scripts": {
    "start": "node src/broker.js",
    "dev":   "node --watch src/broker.js",  ← hot-reload nativo Node 18+
    "test":  "vitest"
  },
  "dependencies": {
    "moleculer": "^0.14.0",
    "bullmq":    "^5.0.0",
    "ioredis":   "^5.0.0",
    "pg":        "^8.0.0",
    "dotenv":    "^16.0.0"
  }
}
imports.js — ESM vs CJS + builtins con prefijo node:ESM
// ── ESM (moderno — usar siempre) ──────────────────────
import { createServer }    from 'node:http';       // builtin con prefijo node:
import { readFile }        from 'node:fs/promises';
import express             from 'express';         // paquete npm
import { config }          from './config.js';     // local — extensión .js obligatoria

export const VERSION = '2.0.0';
export default function iniciar() { /* ... */ }

// Top-level await — solo en ESM
const datos = await readFile('config.json', 'utf8');

// ── CommonJS (legacy — evitar en proyectos nuevos) ────
// const http   = require('http');           ← CJS
// const config = require('./config');       ← sin extensión
// module.exports = { VERSION, iniciar };    ← CJS export

// ── Módulos builtin más usados ────────────────────────
import path            from 'node:path';           // join, resolve, dirname, extname
import os              from 'node:os';             // cpus, totalmem, homedir
import crypto          from 'node:crypto';         // randomUUID, createHash, pbkdf2
import { EventEmitter } from 'node:events';
import { Worker }       from 'node:worker_threads';
bash — comandos NPM esenciales
$ npm install express # instalar dependencia de producción
$ npm install -D vitest # solo en desarrollo
$ npm ci # install limpio desde package-lock.json (CI/CD)
$ npm run dev # ejecutar script "dev"
$ npm outdated # ver paquetes desactualizados
$ npm audit # revisar vulnerabilidades

// 04 — FILESYSTEM

Sistema de Archivos

Node expone el FS del OS a través de node:fs. Usa siempre la API de promesas (fs/promises) — más limpia y compatible con async/await.

filesystem.js — fs/promises + __dirname en ESMFS
import { readFile, writeFile, readdir, mkdir, stat } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

// En ESM no hay __dirname — se construye así:
const __filename = fileURLToPath(import.meta.url);
const __dirname  = path.dirname(__filename);

// ── Leer archivo ──────────────────────────────────────
const raw      = await readFile(path.join(__dirname, '../data/productos.json'), 'utf8');
const productos = JSON.parse(raw);

// ── Escribir archivo ──────────────────────────────────
await writeFile(
  path.join(__dirname, '../logs/sync.json'),
  JSON.stringify({ ts: Date.now(), status: 'ok' }, null, 2)
);

// ── Listar directorio y cargar servicios dinámicamente
async function cargarServicios(dir) {
  const files = await readdir(dir);
  return Promise.all(
    files
      .filter(f => f.endsWith('.service.js'))
      .map(f => import(path.join(dir, f)))     // import dinámico
  );
}

// ── Crear directorio recursivo ────────────────────────
await mkdir(path.join(__dirname, '../logs/2024/01'), { recursive: true });

// ── Metadata: tamaño y fecha de modificación ─────────
const info = await stat('./package.json');
console.log(info.size, info.mtime);

// 05 — HTTP NATIVO

Servidor HTTP Nativo

Node incluye node:http para crear servidores sin dependencias. Entenderlo es la base para cualquier framework web.

server-nativo.js — HTTP desde cero, sin frameworkHTTP
import { createServer } from 'node:http';
import crypto            from 'node:crypto';

const server = createServer(async (req, res) => {
  const { method, url } = req;

  const json = (data, status = 200) => {
    res.writeHead(status, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(data));
  };

  const leerBody = () => new Promise(resolve => {
    let body = '';
    req.on('data', chunk => body += chunk);
    req.on('end',  () => resolve(JSON.parse(body || '{}')));
  });

  if (method === 'GET'  && url === '/health')
    return json({ status: 'ok', uptime: process.uptime() });

  if (method === 'POST' && url === '/ventas') {
    const body = await leerBody();
    return json({ id: crypto.randomUUID(), ...body }, 201);
  }

  json({ error: 'Not Found' }, 404);
});

server.listen(4000, () => console.log('🟢 http://localhost:4000'));
Simulador servidor HTTP — envía peticiones al servidor▶ LIVE
// servidor — log
🟢 Escuchando en :4000
Esperando peticiones…
// cliente — respuesta
Envía una petición →

// 06 — EXPRESS

Express.js — Framework HTTP

El framework HTTP más popular de Node.js. Añade routing, middleware y helpers sobre el HTTP nativo. Minimalista, sin opinión, extensible.

app.js + routes/productos.js — servidor Express completoEXPRESS
import express from 'express';
import { productosRouter } from './routes/productos.js';
import { authMiddleware, logger, errorHandler } from './middleware/index.js';

const app = express();

// Middleware global
app.use(express.json());
app.use(logger);
app.use(authMiddleware);

// Rutas
app.get('/health', (req, res) =>
  res.json({ status: 'ok', uptime: process.uptime() })
);
app.use('/api/v1/productos', productosRouter);

// Error handler (siempre al final, 4 params)
app.use(errorHandler);

app.listen(process.env.PORT ?? 3000);

// ── routes/productos.js ───────────────────────────────
export const productosRouter = express.Router();

productosRouter
  .get('/',      listarProductos)
  .get('/:id',   obtenerProducto)
  .post('/',     crearProducto)
  .put('/:id',   actualizarProducto)
  .delete('/:id', eliminarProducto);

async function listarProductos(req, res, next) {
  try {
    const { pagina = 1, limite = 20, busqueda } = req.query;
    const datos = await productosService.listar({ pagina, limite, busqueda });
    res.json({ data: datos.rows, total: datos.count, pagina: +pagina });
  } catch (err) {
    next(err); // pasa al error handler global
  }
}

async function obtenerProducto(req, res, next) {
  try {
    const producto = await productosService.porId(req.params.id);
    if (!producto) return res.status(404).json({ error: 'No encontrado' });
    res.json(producto);
  } catch (err) { next(err); }
}

// 07 — MIDDLEWARE

Middleware — Pipeline

Un middleware es una función (req, res, next). Express procesa cada petición pasándola por una cadena de middlewares en orden. Cada uno puede modificar req/res o llamar next().

middleware/ — auth JWT, logger, rate-limit, errorsMW
// ── Auth middleware — verifica JWT ────────────────────
export function authMiddleware(req, res, next) {
  const token = req.headers['authorization']?.split(' ')[1];
  const publicas = ['/health', '/api/v1/auth/login'];
  if (publicas.includes(req.path)) return next();
  if (!token) return res.status(401).json({ error: 'Token requerido' });
  try {
    req.usuario = verificarJWT(token);
    next();
  } catch {
    res.status(401).json({ error: 'Token inválido' });
  }
}

// ── Logger — mide tiempo de respuesta ────────────────
export function logger(req, res, next) {
  const t0 = Date.now();
  res.on('finish', () =>
    console.log(`${req.method} ${req.path} ${res.statusCode} — ${Date.now()-t0}ms`)
  );
  next();
}

// ── Rate limit — max peticiones por IP ───────────────
export function rateLimit(max = 100, windowMs = 60_000) {
  const contadores = new Map();
  return (req, res, next) => {
    const ip    = req.ip;
    const ahora = Date.now();
    const datos = contadores.get(ip) ?? { count: 0, desde: ahora };
    if (ahora - datos.desde > windowMs) { datos.count = 0; datos.desde = ahora; }
    datos.count++;
    contadores.set(ip, datos);
    if (datos.count > max)
      return res.status(429).json({ error: 'Too Many Requests' });
    next();
  };
}

// ── Error handler global — 4 parámetros (req, res, err, next)
export function errorHandler(err, req, res, next) {
  console.error(err);
  res.status(err.status ?? 500).json({ error: err.message });
}
ℹ️
El orden de los middlewares importaExpress ejecuta los middlewares en el orden en que se registran con app.use(). El error handler siempre va al final. Los middlewares de autenticación van antes que las rutas que protegen.

// 08 — STREAMS

Streams — Datos en Flujo

Los Streams procesan datos en chunks en lugar de cargar todo en memoria. Esenciales para archivos grandes, importaciones CSV y exportaciones de reportes.

Readable Writable Transform pipeline() for await...of
streams.js — procesar CSV grande sin agotar memoriaSTREAMS
import { createReadStream }     from 'node:fs';
import { Transform, pipeline }  from 'node:stream';
import { promisify }             from 'node:util';
import { createInterface }       from 'node:readline';

const pipelineAsync = promisify(pipeline);

// ── Leer CSV línea por línea (memoria constante) ──────
async function procesarCSV(ruta) {
  const rl = createInterface({ input: createReadStream(ruta) });
  let headers = [], lineaNum = 0, errores = [];

  for await (const linea of rl) {
    if (lineaNum === 0) { headers = linea.split(','); lineaNum++; continue; }
    const fila = Object.fromEntries(headers.map((h,i) => [h, linea.split(',')[i]]));
    try { await insertarProducto(fila); }
    catch(e) { errores.push({ linea: lineaNum, error: e.message }); }
    lineaNum++;
  }
  return { procesadas: lineaNum - 1, errores };
}

// ── Exportar CSV al cliente HTTP con Transform stream
async function exportarVentas(res) {
  res.setHeader('Content-Type', 'text/csv');
  res.setHeader('Content-Disposition', 'attachment; filename="ventas.csv"');

  const transform = new Transform({
    objectMode: true,
    transform(venta, enc, cb) {
      cb(null, `${venta.id},${venta.fecha},${venta.total}\n`);
    },
  });

  await pipelineAsync(
    streamVentasDB(), // Readable de BD
    transform,          // objeto → línea CSV
    res                 // Writable — escribe al cliente HTTP
  );
}
💡
pipeline() en lugar de pipe()pipe() no maneja errores correctamente — si el stream destino falla, el origen no se cierra y hay memory leak. pipeline() cierra todos los streams en la cadena si cualquiera falla. En producción: siempre pipeline().

// 09 — WORKER THREADS

Worker Threads

Para tareas CPU-intensivas que bloquearían el Event Loop: encriptación pesada, parsing de XML/CSV grande, cálculos de nómina complejos.

workers/nomina.worker.js + uso desde servicio principalWORKERS
// ── workers/nomina.worker.js ──────────────────────────
import { parentPort, workerData } from 'node:worker_threads';

const { empleados, periodo } = workerData;

// Este código corre en hilo separado — no bloquea el Event Loop
const calculos = empleados.map(emp => ({
  id:           emp.id,
  salarioBruto: emp.diasTrabajados * emp.salarioDiario,
  imss:         emp.salarioBruto * 0.0192,
  isr:          calcularISR(emp.salarioBruto),
  neto:         emp.salarioBruto - emp.imss - emp.isr,
}));

parentPort.postMessage({ calculos, periodo });

// ── servicios/nomina.service.js — uso del worker ─────
import { Worker } from 'node:worker_threads';

function calcularNominaEnWorker(empleados, periodo) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(
      './workers/nomina.worker.js',
      { workerData: { empleados, periodo } }
    );
    worker.on('message', resolve);
    worker.on('error',   reject);
    worker.on('exit', code => {
      if (code !== 0) reject(new Error(`Worker terminó con código ${code}`));
    });
  });
}

// El hilo principal sigue atendiendo peticiones HTTP
// mientras el worker calcula la nómina en paralelo
const resultado = await calcularNominaEnWorker(empleados, '2024-01');
⚠️
Worker Threads vs child_processWorker Threads comparten memoria (SharedArrayBuffer) con el hilo principal — más eficiente para datos grandes. child_process.fork() lanza un proceso Node completamente separado — más aislado pero con mayor overhead. Para el ERP: Workers para nómina y reportes pesados, child_process solo si necesitas aislamiento total de fallos.

// 10 — ENV & CONFIG

Variables de Entorno

Credenciales y configuración sensible nunca van en el código. Se inyectan como variables de entorno en producción y se leen desde .env en desarrollo.

.env + config/index.js — validación y tipado centralizadosENV
// ── .env (nunca commitear — en .gitignore) ────────────
// NODE_ENV=development
// PORT=4000
// DB_HOST=localhost
// DB_PASSWORD=supersecreta123
// REDIS_URL=redis://localhost:6379
// JWT_SECRET=clave-muy-larga-y-aleatoria-minimo-32-chars

// ── config/index.js — valida al arranque ──────────────
import 'dotenv/config';   // carga .env en process.env

function requerida(nombre) {
  const val = process.env[nombre];
  if (!val) throw new Error(`Variable requerida: ${nombre}`);
  return val;
}

export const config = {
  env:    process.env.NODE_ENV ?? 'development',
  port:   Number(process.env.PORT ?? 4000),
  isdev:  process.env.NODE_ENV !== 'production',

  db: {
    host:     requerida('DB_HOST'),
    port:     Number(process.env.DB_PORT ?? 5432),
    database: requerida('DB_NAME'),
    user:     requerida('DB_USER'),
    password: requerida('DB_PASSWORD'),
    ssl:      process.env.NODE_ENV === 'production',
  },

  redis: {
    url:      requerida('REDIS_URL'),
    password: process.env.REDIS_PASSWORD,
  },

  jwt: {
    secret:  requerida('JWT_SECRET'),
    expires: process.env.JWT_EXPIRES ?? '15m',
  },
};

// Si falta alguna variable requerida, el proceso lanza
// un error al arrancar — falla rápido, nunca en runtime

// 11 — MANEJO DE ERRORES

Manejo de Errores

Un error sin manejar puede tumbar el proceso completo. Estrategia: capturar a nivel de función, errores tipados con contexto, handlers globales como red de seguridad.

errors.js — clases tipadas + handlers + graceful shutdownERRORS
// ── Errores personalizados con status y código ────────
export class AppError extends Error {
  constructor(message, status = 500, code = 'INTERNAL_ERROR') {
    super(message);
    this.name   = this.constructor.name;
    this.status = status;
    this.code   = code;
    Error.captureStackTrace(this, this.constructor);
  }
}
export class NotFoundError   extends AppError { constructor(m) { super(m, 404, 'NOT_FOUND'); } }
export class ValidationError extends AppError { constructor(m) { super(m, 422, 'VALIDATION'); } }
export class UnauthorizedError extends AppError { constructor(m) { super(m, 401, 'UNAUTHORIZED'); } }

// ── Handlers globales — red de seguridad ──────────────
process.on('uncaughtException', (err) => {
  console.error('💥 uncaughtException:', err);
  process.exit(1); // estado del proceso es incierto, salir limpio
});

process.on('unhandledRejection', (reason) => {
  console.error('💥 unhandledRejection:', reason);
  // Node 20 ya lo convierte en uncaughtException automáticamente
});

// ── Graceful shutdown — cerrar conexiones limpiamente ─
async function shutdown(signal) {
  console.log(`🛑 ${signal} — cerrando limpiamente…`);
  await broker.stop();     // Moleculer
  await db.end();          // PostgreSQL pool
  await redis.quit();      // Redis
  process.exit(0);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));  // Docker / Kubernetes
process.on('SIGINT',  () => shutdown('SIGINT'));   // Ctrl+C

// 12 — PERFORMANCE

Performance y Diagnóstico

Medir antes de optimizar. Node expone perf_hooks para instrumentar código, y process.memoryUsage() / cpuUsage() para métricas del proceso.

performance.js — métricas, cluster multi-corePERF
import { performance } from 'node:perf_hooks';
import cluster          from 'node:cluster';
import os               from 'node:os';

// ── Medir tiempo de operación ─────────────────────────
async function medir(nombre, fn) {
  const t0 = performance.now();
  const r  = await fn();
  console.log(`[PERF] ${nombre}: ${(performance.now()-t0).toFixed(2)}ms`);
  return r;
}

// ── Métricas del proceso ──────────────────────────────
export function metricas() {
  const m = process.memoryUsage();
  return {
    heapUsed:  Math.round(m.heapUsed  / 1024 / 1024) + ' MB',
    heapTotal: Math.round(m.heapTotal / 1024 / 1024) + ' MB',
    rss:       Math.round(m.rss        / 1024 / 1024) + ' MB',
    uptime:    Math.round(process.uptime()) + 's',
  };
}

// ── Cluster — usar todos los CPUs disponibles ─────────
if (cluster.isPrimary) {
  const numCPUs = os.cpus().length;
  console.log(`Iniciando ${numCPUs} workers`);
  for (let i = 0; i < numCPUs; i++) cluster.fork();
  cluster.on('exit', (worker) => {
    console.log(`Worker ${worker.process.pid} caído — reiniciando`);
    cluster.fork(); // auto-restart
  });
} else {
  await iniciarApp(); // cada worker corre la app normalmente
}
TécnicaCuándo usarHerramienta
ClusterHTTP API multi-core en producciónnode:cluster
Worker ThreadsCPU-bound: nómina, encriptación, parsingnode:worker_threads
StreamsArchivos grandes, export CSV/XLSXnode:stream
CachingRespuestas repetidas de BD o APIsRedis + ioredis
Connection PoolReutilizar conexiones a PostgreSQLpg.Pool(max:20)
ProfilingEncontrar cuellos de botella reales--inspect + Chrome DevTools

// 13 — NODE EN EL ERP

Node.js en el ERP Carnicerías

Node es el runtime de todo el backend: Moleculer corre sobre Node, los Workers de BullMQ corren sobre Node, el sync de sucursales usa Node. Este es el punto de entrada real del sistema.

src/index.js — arranque completo del backend ERP (Node + Moleculer + BullMQ)REAL
import 'dotenv/config';
import { ServiceBroker }  from 'moleculer';
import { readdir }        from 'node:fs/promises';
import path               from 'node:path';
import { fileURLToPath }  from 'node:url';
import { config }         from './config/index.js';
import { iniciarWorkers } from './workers/index.js';
import { metricas }       from './utils/metrics.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

async function main() {
  console.log(`🥩 Iniciando ERP — nodo: ${config.nodeId}`);

  // 1. Crear broker Moleculer con Redis como transporter
  const broker = new ServiceBroker({
    nodeID:      config.nodeId,
    transporter: config.redis.url,
    cacher:      'redis',
  });

  // 2. Cargar servicios automáticamente desde el directorio
  const servicesDir = path.join(__dirname, 'services');
  const files = await readdir(servicesDir);

  for (const file of files.filter(f => f.endsWith('.service.js'))) {
    const { default: schema } = await import(path.join(servicesDir, file));
    broker.createService(schema);
    console.log(`  ✓ Servicio: ${file}`);
  }

  // 3. Arrancar broker (Service Discovery via Redis)
  await broker.start();

  // 4. Iniciar BullMQ workers (CFDI, nómina, reportes, sync)
  await iniciarWorkers(broker);

  // 5. Reporte de métricas cada 5 minutos
  setInterval(() => console.log('[STATUS]', metricas()), 5 * 60_000);

  console.log('🟢 Sistema listo — todos los servicios activos');
}

// Graceful shutdown al recibir señales del SO/Docker/K8s
const shutdown = async (sig) => {
  console.log(`🛑 ${sig} — apagando broker y workers…`);
  await broker.stop();
  process.exit(0);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT',  () => shutdown('SIGINT'));

main().catch(err => { console.error(err); process.exit(1); });
🥩
De Node al stack completoNode es la base de todo. Con esto dominado, Moleculer añade Service Discovery y balanceo de carga automático sobre el Event Loop de Node. BullMQ usa el mismo modelo asíncrono para procesar colas. PostgreSQL, Redis y las APIs de AWS se consumen todas a través de los mismos patrones async/await que aprendiste aquí.