En la lección anterior consumiste streams que ya existían: createReadStream producía trozos y createWriteStream los recibía. Entendiste la contrapresión, viste que pipe() la gestiona por ti y descubriste su defecto: no propaga errores ni limpia lo que deja atrás.
Ahora toca la otra mitad. Vas a escribir tus propios streams —el eslabón intermedio que convierte una cosa en otra— y a montar la tubería completa de proceso de Escena Viva: leer datos/ventas.csv, convertir cada línea en un objeto venta, filtrar por sala, agregar por sesión y escribir el resultado, todo con memoria constante, sea el fichero de dos kilobytes o de ochocientos megabytes. Y sustituirás pipe() por stream.pipeline, la herramienta de verdad: propaga errores, destruye toda la cadena cuando algo falla y tiene versión con promesas. Al final verás la forma más legible de escribir transformaciones, que no es una clase sino un generador asíncrono.
Contenido
objectMode: streams que transportan objetos- El stream
Transform:_transformy_flush - Un
Transformque parsea líneas CSV ReadableyWritablea medidastream.pipelinefrente apipe()- La tubería de proceso de Escena Viva
- Compresión con
zliben la tubería Readable.from()y los generadores asíncronos- Errores y cancelación con
AbortSignal
objectMode: streams que transportan objetos
objectMode: streams que transportan objetosPor defecto, un stream transporta bytes: Buffer o cadenas. Eso es lo correcto para ficheros y sockets, pero inútil cuando lo que fluye por la tubería son ventas, eventos o registros ya interpretados. objectMode: true cambia esa naturaleza:
| Modo binario (por defecto) | objectMode: true |
|
|---|---|---|
| Qué viaja | Buffer o string |
Cualquier valor salvo null |
Qué cuenta el highWaterMark |
Bytes (64 KB) | Número de objetos (16) |
| Trozos | Se parten y fusionan | Cada push es un elemento indivisible |
El modo se puede declarar por lado: new Transform({ readableObjectMode: true, writableObjectMode: false }) describe exactamente lo que hace un parser —recibe texto y emite objetos—, mientras que objectMode: true a secas activa los dos lados. Y el null merece un aviso: en un stream, push(null) significa "se acabó", así que no puedes enviar null como dato, ni siquiera en modo objeto; para representar la ausencia de algo, usa undefined o un objeto marcador.
- El stream
Transform: _transform y _flush
Transform: _transform y _flushUn Transform es un stream que lee por un lado, hace algo y escribe por el otro. Se implementa con dos métodos:
| Método | Cuándo se llama | Para qué |
|---|---|---|
_transform(trozo, codificacion, callback) |
Por cada trozo | Procesar y emitir con this.push(...) |
_flush(callback) |
Al cerrarse la entrada | Emitir lo que quedara pendiente |
La mecánica es siempre la misma: _transform recibe un trozo, hace su trabajo, emite cero o más resultados con this.push(...) y avisa de que ha terminado llamando a callback(); _flush se ejecuta una única vez al final, cuando ya no va a entrar nada más, y es la última oportunidad de emitir.
Tres reglas que no admiten excepción:
- Hay que llamar a
callback()exactamente una vez por cada_transform. Si no lo llamas, la tubería se cuelga para siempre sin ningún mensaje; si lo llamas dos veces, Node lanzaERR_MULTIPLE_CALLBACK. - Los errores se señalan con
callback(error), no conthrow: una excepción lanzada en un contexto asíncrono no la captura nadie. _flushes donde vive el estado acumulado. Todo lo que no puedas emitir hasta ver el final —la última línea sin salto, el total de un agregado, el cierre de un JSON— sale ahí. Eso es lo que distingue unTransformde un simplemap: puede acumular entre trozos.
- Un
Transform que parsea líneas CSV
Transform que parsea líneas CSVAquí está el problema real: los trozos del fichero no coinciden con las líneas. Un Transform con estado resuelve troceado y conversión en un solo paso.
// src/streams/parsear-ventas.js
// Transform: recibe texto de datos/ventas.csv y emite objetos venta.
const { Transform } = require('node:stream');
class ParseadorDeVentas extends Transform {
#resto = ''; // trozo de linea que quedo a medias
#primera = true; // para saltar la cabecera
#descartadas = 0;
constructor(opciones = {}) {
// Entra texto, salen objetos: los dos lados son distintos.
super({ ...opciones, writableObjectMode: false, readableObjectMode: true });
}
#convertir(linea) {
const [codigoEntrada, sesionId, fechaVenta, precioCentimos, canal] = linea.split(',');
if (!codigoEntrada || !sesionId || Number.isNaN(Number(precioCentimos))) {
this.#descartadas += 1; // linea corrupta: se ignora, no aborta
return;
}
this.push({ codigoEntrada, sesionId, fechaVenta, precioCentimos: Number(precioCentimos), canal });
}
_transform(trozo, codificacion, callback) {
const lineas = (this.#resto + trozo).split('\n');
this.#resto = lineas.pop(); // la ultima puede estar cortada
for (const linea of lineas) {
if (this.#primera) { this.#primera = false; continue; } // cabecera
if (linea.trim() !== '') this.#convertir(linea);
}
callback();
}
_flush(callback) {
// La ultima linea, si el fichero no termina en salto.
if (this.#resto.trim() !== '') this.#convertir(this.#resto);
if (this.#descartadas > 0) console.error(`[ventas] ${this.#descartadas} descartadas`);
callback();
}
}
module.exports = { ParseadorDeVentas };Fíjate en el papel de #resto: es el problema que readline resolvía por nosotros en la lección anterior, ahora resuelto a mano porque necesitamos emitir objetos, no líneas. Y en _flush se procesa ese resto pendiente: sin esa línea, la última venta se perdería en silencio siempre que el fichero no termine en salto. Es un error clásico y difícil de detectar, porque falla en un registro de un millón.
Readable y Writable a medida
Readable y Writable a medidaUn Readable produce datos. Se implementa con _read(), que Node llama cuando quiere más material, y dentro se emite con this.push(valor); cuando ya no hay nada que producir, this.push(null) señala el final del stream. En la práctica casi nunca hace falta escribir uno: Readable.from() (apartado 8) cubre casi todos los casos con una línea.
Un Writable consume datos. Implementa _write(trozo, codificacion, callback), y ese callback es la pieza clave: hasta que no lo llamas, el stream considera que sigues ocupado. Ahí es donde nace la contrapresión que sentiste en la lección anterior.
const { Writable } = require('node:stream');
class AcumuladorPorSesion extends Writable {
#porSesion = new Map();
constructor() { super({ objectMode: true }); }
_write(venta, codificacion, callback) {
const acumulado = this.#porSesion.get(venta.sesionId) ??
{ sesionId: venta.sesionId, entradas: 0, recaudacionCentimos: 0 };
acumulado.entradas += 1;
acumulado.recaudacionCentimos += venta.precioCentimos;
this.#porSesion.set(venta.sesionId, acumulado);
callback(); // listo para el siguiente
}
get resultado() {
return [...this.#porSesion.values()].sort((a, b) => a.sesionId.localeCompare(b.sesionId));
}
}Si el trabajo de _write fuera asíncrono —guardar en base de datos, llamar a una API—, bastaría con llamar al callback al terminar: la tubería entera se frena sola mientras tanto. Existe además _writev para procesar varios elementos de golpe, útil al insertar por lotes.
stream.pipeline frente a pipe()
stream.pipeline frente a pipe()Ya viste el problema de pipe(). stream.pipeline lo arregla, y su versión con promesas —const { pipeline } = require('node:stream/promises')— es la que usaremos siempre.
pipe() |
pipeline() |
|
|---|---|---|
| Contrapresión | Sí | Sí |
| Propaga errores por la cadena | No | Sí: el fallo de una etapa aborta todo |
| Destruye los streams al fallar | No: quedan descriptores abiertos | Sí: destroy() en todos |
| Avisa de que ha terminado | Escuchando finish a mano |
La promesa se resuelve |
| Cancelación / errores | No / un on('error') por stream |
AbortSignal / un try/catch |
La diferencia práctica es enorme:
// Con pipe: un on('error') por etapa y limpieza manual.
origen.on('error', gestionar);
transformacion.on('error', gestionar);
destino.on('error', gestionar);
origen.pipe(transformacion).pipe(destino);
// Con pipeline: una linea y un try/catch.
try {
await pipeline(origen, transformacion, destino);
} catch (error) {
console.error(`[tuberia] fallo: ${error.message}`);
}Regla del curso: nunca pipe() en código de producción. pipe sirve para explicar el concepto; pipeline es lo que se escribe.
- La tubería de proceso de Escena Viva
Con las piezas anteriores montamos el proceso completo: del CSV crudo al informe en JSON, filtrando por sala y sin cargar nunca el fichero entero.
// src/informes/tuberia-ventas.js
// CSV de ventas -> objetos -> filtro por sala -> agregacion -> JSON.
const fs = require('node:fs');
const path = require('node:path');
const { Transform } = require('node:stream');
const { pipeline } = require('node:stream/promises');
const { ParseadorDeVentas } = require('../streams/parsear-ventas.js');
const { obtenerCatalogo } = require('../catalogo-datos.js');
const { FICHERO_VENTAS, DIRECTORIO_INFORMES } = require('../config/rutas.js');
const { escribirAtomico } = require('../utiles/escritura-atomica.js');
// Transform de objeto a objeto: deja pasar solo las sesiones permitidas.
// Con la forma abreviada: opciones y metodo transform sin subclase.
const filtrarPorSesiones = (permitidas) => new Transform({
objectMode: true,
transform(venta, codificacion, callback) {
if (permitidas.has(venta.sesionId)) this.push(venta);
callback();
}
});
async function generarVentasPorSesion({ sala = null } = {}) {
// El cruce con el catalogo se hace UNA vez, no por linea del CSV.
const eventos = await obtenerCatalogo();
const relevantes = sala ? eventos.filter((e) => e.sala === sala) : eventos;
const sesionesPermitidas = new Set(relevantes.flatMap((e) => e.sesiones.map((s) => s.id)));
const acumulador = new AcumuladorPorSesion();
await pipeline(
fs.createReadStream(FICHERO_VENTAS, { encoding: 'utf8' }),
new ParseadorDeVentas(),
filtrarPorSesiones(sesionesPermitidas),
acumulador
);
const sesiones = acumulador.resultado;
const totales = sesiones.reduce((t, s) => ({
entradas: t.entradas + s.entradas,
recaudacionCentimos: t.recaudacionCentimos + s.recaudacionCentimos
}), { entradas: 0, recaudacionCentimos: 0 });
const informe = { generadoEn: new Date().toISOString(), sala: sala ?? 'todas', sesiones, totales };
const ruta = path.join(DIRECTORIO_INFORMES, 'ventas-por-sesion.json');
await escribirAtomico(ruta, JSON.stringify(informe, null, 2));
return { ruta, ...totales };
}
module.exports = { generarVentasPorSesion };node -e "require('./src/informes/tuberia-ventas.js').generarVentasPorSesion().then(console.log)"
# { ruta: '.../informes/ventas-por-sesion.json', entradas: 1811, recaudacionCentimos: 5389800 }Las 1811 entradas cuadran con la semilla, que es la comprobación que buscábamos. Y observa la forma de la tubería: cuatro etapas, cada una con una sola responsabilidad, encadenadas en un pipeline que se lee de arriba abajo; cambiar el filtro, añadir una validación o escribir a otro destino es tocar una línea. Un detalle de diseño: el AcumuladorPorSesion va al final porque una agregación no puede emitir nada hasta ver el último elemento; si necesitaras continuar la tubería después de agregar, sería un Transform que emite su resultado en _flush.
- Compresión con
zlib en la tubería
zlib en la tuberíaArchivar el histórico es intercalar una etapa más: zlib.createGzip() es un Transform como los tuyos.
// src/informes/archivar-ventas.js
const fs = require('node:fs');
const zlib = require('node:zlib');
const { pipeline } = require('node:stream/promises');
async function archivarVentas(rutaOrigen, rutaDestino) {
await pipeline(
fs.createReadStream(rutaOrigen),
zlib.createGzip({ level: 9 }), // 1 = rapido, 9 = maxima compresion
fs.createWriteStream(rutaDestino)
);
}Aquí no se ha indicado encoding en el lector: los datos viajan como Buffer de principio a fin, que es lo correcto para comprimir —convertirlos a texto y de vuelta a bytes no aportaría nada y costaría CPU—. La descompresión es la misma tubería al revés: basta con intercalar zlib.createGunzip() entre el lector del .gz y el parser para procesar el histórico comprimido sin descomprimirlo a disco. Eso es lo que hace potente el modelo: las etapas se combinan sin que ninguna sepa nada de las demás, y el parser no tiene ni idea de que sus bytes vienen de un fichero comprimido.
Readable.from() y los generadores asíncronos
Readable.from() y los generadores asíncronosReadable.from() convierte cualquier iterable —array, Map, generador— en un stream:
const { Readable } = require('node:stream');
// Un array cualquiera, ya como stream en modo objeto.
const ventas = Readable.from([{ sesionId: 'ses-001-1' }, { sesionId: 'ses-002-1' }]);Es utilísimo para las pruebas del Módulo 9: alimentas una tubería con datos de mentira sin tocar el disco. Pero lo mejor viene ahora: un generador asíncrono puede usarse directamente como etapa de un pipeline, y es la forma más legible de escribir una transformación.
// La misma logica del ParseadorDeVentas, sin clase y sin callbacks.
// convertir(linea) devuelve el objeto venta, como el metodo privado de antes.
async function* parsearVentas(origen) {
let resto = '';
let primera = true;
for await (const trozo of origen) {
const lineas = (resto + trozo).split('\n');
resto = lineas.pop();
for (const linea of lineas) {
if (primera) { primera = false; continue; }
if (linea.trim() !== '') yield convertir(linea);
}
}
if (resto.trim() !== '') yield convertir(resto); // el equivalente de _flush
}
// Filtro: tres lineas, sin clase, sin objectMode explicito.
const filtrarPorSesiones = (permitidas) => async function* (origen) {
for await (const venta of origen) {
if (permitidas.has(venta.sesionId)) yield venta;
}
};
await pipeline(
fs.createReadStream(FICHERO_VENTAS, { encoding: 'utf8' }),
parsearVentas,
filtrarPorSesiones(sesionesPermitidas),
acumulador
);La comparación es reveladora:
Clase Transform |
Generador asíncrono | |
|---|---|---|
| Líneas de código | Constructor, _transform, _flush |
Muchas menos |
objectMode |
Hay que declararlo | Implícito |
| Fin de la entrada | _flush |
El código tras el for await |
| Estado entre trozos | Campo de la clase | Variable local |
await dentro / reutilizable como objeto |
Incómodo / sí | Natural / no |
Para transformaciones nuevas, empieza siempre por un generador asíncrono. Recurre a la clase Transform cuando necesites un objeto con estado accesible desde fuera —como nuestro AcumuladorPorSesion, cuyo resultado se lee al terminar— o cuando tengas que integrarte con una API que espera un stream concreto.
- Errores y cancelación con
AbortSignal
AbortSignalUn fallo en cualquier etapa aborta la tubería y rechaza la promesa; lo que llega al catch es el error original, con su code intacto:
try {
await pipeline(origen, parsearVentas, acumulador);
} catch (error) {
// Casos previstos: se informan y se degradan. El resto se propaga.
if (error.code === 'ENOENT') return avisar('no existe el fichero de ventas');
if (error.name === 'AbortError') return avisar('proceso cancelado');
throw error;
}Y pipeline acepta una señal de cancelación, igual que fetch:
const controlador = new AbortController();
// Un informe no puede tardar mas de treinta segundos.
const limite = setTimeout(() => controlador.abort(), 30_000);
try {
await pipeline(origen, parsearVentas, acumulador, { signal: controlador.signal });
} finally {
clearTimeout(limite); // no dejar temporizadores vivos
}Al abortar, pipeline destruye todos los streams de la cadena: los descriptores se liberan y la promesa se rechaza con un AbortError. Es exactamente el trabajo que en la lección anterior había que escribir a mano con on('error') cruzados y destroy(). En el Módulo 4 volverás a esto: cuando un cliente HTTP cierra la conexión a mitad de una descarga, lo correcto es abortar la tubería que generaba la respuesta en lugar de seguir trabajando para nadie.
Errores Comunes y Consejos
- Olvidar
callback()en_transformo_write. La tubería se cuelga en silencio, sin error ni traza. Es el fallo número uno. - Usar
throwdentro de_transformen lugar decallback(error), u olvidar_flush, con lo que todo lo acumulado —la última línea, el total, el cierre— se pierde sin avisar. - No declarar
objectModeal emitir objetos: Node protesta conERR_INVALID_ARG_TYPEporque esperaBuffero cadena. Ypush(null)no es un dato: significa fin de stream, siempre. - Seguir usando
pipe(). Si un error de la primera etapa no llega a la última, tienes descriptores filtrados y fallos invisibles. - Consejo: una etapa, una responsabilidad. Un
Transformque parsea, filtra y agrega a la vez es imposible de probar; tres etapas encadenadas se prueban por separado conReadable.from(). Y si dudas entre clase y generador, empieza por el generador: convertirlo en clase después es fácil, al revés no tanto.
Ejercicios
Ejercicio 1: validador en la tubería
Escribe un generador asíncrono validarVentas que se intercale entre el parser y el filtro, y que compruebe cada venta: codigoEntrada con el formato EV-<año>-<6 dígitos>, sesionId con la forma ses-NNN-M, precioCentimos entero positivo y canal entre web, taquilla y app. Las ventas válidas pasan; las inválidas se cuentan y se registran por stderr con su número de línea. Al final debe informar de cuántas se descartaron.
Ejercicio 2: informe por canal comprimido
Escribe src/informes/ventas-por-canal.js con una tubería que lea datos/ventas.csv, agregue por canal (entradas, recaudación y ticket medio) y escriba informes/ventas-por-canal.json.gz comprimido, sin generar el JSON sin comprimir en disco. Pista: Readable.from([JSON.stringify(informe, null, 2)]) convierte el resultado en el origen de una segunda tubería.
Ejercicio 3: tubería con límite de tiempo y reintento
Escribe una función generarConReintento(opciones) que ejecute la tubería de generarVentasPorSesion con un AbortSignal de 10 segundos y, si falla por AbortError o por un error de E/S transitorio, la reintente hasta tres veces usando reintentar y dormir del Módulo 2. No debe reintentar ante ENOENT ni ante datos corruptos: esos fallos no se arreglan repitiendo.
Soluciones
Solución 1. El validador como generador queda casi declarativo:
const PATRON_CODIGO = /^EV-\d{4}-\d{6}$/;
const PATRON_SESION = /^ses-\d{3}-\d+$/;
const CANALES = new Set(['web', 'taquilla', 'app']);
async function* validarVentas(origen) {
let numero = 0;
let descartadas = 0;
for await (const venta of origen) {
numero += 1;
const problemas = [];
if (!PATRON_CODIGO.test(venta.codigoEntrada)) problemas.push('codigo');
if (!PATRON_SESION.test(venta.sesionId)) problemas.push('sesion');
if (!Number.isInteger(venta.precioCentimos) || venta.precioCentimos <= 0) problemas.push('precio');
if (!CANALES.has(venta.canal)) problemas.push('canal');
if (problemas.length === 0) { yield venta; continue; }
descartadas += 1;
console.error(`[validar] venta ${numero} descartada (${problemas.join(', ')})`);
}
if (descartadas > 0) console.error(`[validar] ${descartadas} ventas descartadas`);
}El bloque final, tras el for await, es el equivalente exacto del _flush de una clase Transform: se ejecuta al agotarse la entrada. Si necesitaras parametrizar el validador, envuélvelo en una función que devuelva el generador, como el filtro del apartado 8.
Solución 2. Dos tuberías encadenadas, la segunda con Readable.from:
const acumulador = new AcumuladorPorCanal();
await pipeline(fs.createReadStream(FICHERO_VENTAS, { encoding: 'utf8' }), parsearVentas, acumulador);
const informe = acumulador.resultado.map((c) => ({
...c,
ticketMedioCentimos: Math.round(c.recaudacionCentimos / c.entradas)
}));
await pipeline(
Readable.from([JSON.stringify(informe, null, 2)]),
zlib.createGzip(),
fs.createWriteStream(path.join(DIRECTORIO_INFORMES, 'ventas-por-canal.json.gz'))
);El JSON nunca toca el disco sin comprimir: sale de memoria como stream, pasa por el compresor y llega al fichero. Con un informe pequeño da igual; con uno de cientos de megabytes es la diferencia entre necesitar el doble de espacio libre o no.
Solución 3. La clave es clasificar los errores antes de decidir si reintentar:
const NO_REINTENTABLES = new Set(['ENOENT', 'EACCES', 'DATOS_CORRUPTOS']);
const generarConReintento = (opciones = {}) => reintentar(async () => {
const controlador = new AbortController();
const limite = setTimeout(() => controlador.abort(), 10_000);
try {
return await generarVentasPorSesion({ ...opciones, signal: controlador.signal });
} catch (error) {
// Marcamos los fallos definitivos para que reintentar no insista.
if (NO_REINTENTABLES.has(error.code ?? error.codigo)) error.definitivo = true;
throw error;
} finally {
clearTimeout(limite);
}
}, { intentos: 3, esperaMs: 500, debeReintentar: (e) => !e.definitivo });Reintentar un ENOENT es inútil: el fichero no va a aparecer solo y solo consigues retrasar el mensaje de error tres veces. Un AbortError por límite de tiempo o un EBUSY transitorio, en cambio, sí pueden resolverse en el segundo intento. Reintentar sin clasificar el error es de las formas más caras de esconder un problema.
Conclusión
Ya no solo consumes streams: los escribes. Sabes que objectMode convierte una tubería de bytes en una tubería de objetos, que el highWaterMark pasa a contar elementos y que readableObjectMode y writableObjectMode se declaran por separado porque un parser recibe texto y emite objetos. Dominas el Transform con _transform —llamando a callback() exactamente una vez y señalando los errores con callback(error)— y _flush, donde se emite todo lo acumulado, incluida esa última línea sin salto que se pierde en silencio cuando falta. Has construido un parser de CSV con estado, un Writable agregador y un filtro, y los has encadenado en la tubería completa de Escena Viva: CSV → objetos → filtro por sala → agregación → JSON, con las 1811 ventas cuadrando con la semilla y memoria constante en todo el recorrido. Y has sustituido pipe() por stream.pipeline de node:stream/promises, que propaga errores por toda la cadena, destruye todos los streams al fallar, se integra con try/catch y acepta AbortSignal. La regla es firme: pipe para explicar, pipeline para trabajar.
Has visto además cómo zlib.createGzip se intercala como una etapa más —y cómo una tubería puede leer directamente de un .gz sin descomprimir a disco—, cómo Readable.from() convierte cualquier iterable en un stream, y por qué un generador asíncrono es casi siempre mejor que una clase Transform: menos código, objectMode implícito, await natural y el fin de la entrada tratado sin ceremonias. Y sabes cancelar una tubería con AbortSignal sin dejar descriptores vivos.
Queda una pieza en el fondo de todo esto. Cada vez que hemos escrito { encoding: 'utf8' } estábamos pidiendo una traducción de bytes a texto, y cada vez que la hemos omitido —al comprimir, al copiar— los datos viajaban como Buffer. En Buffers y Datos Binarios miraremos por fin dentro de esa caja: qué es un Buffer y por qué vive fuera del montículo de V8, por qué allocUnsafe puede mostrarte memoria de otro, las codificaciones y sus usos, el orden de bytes, el error clásico de slice compartiendo memoria, por qué un emoji rompe un troceado ingenuo y cómo lo evita StringDecoder. Y lo aplicaremos a Escena Viva: detectar por sus números mágicos si el cartel que sube un organizador es de verdad un PNG, y codificar en base64url el código QR de una entrada.
Curso de Node.js: De Principiante a Avanzado
Módulo 1: Introducción a Node.js
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
