Node.js es un runtime de JavaScript fuera del navegador, construido sobre V8 y ampliado con APIs para red, archivos, procesos, streams y acceso al sistema operativo. Se usa habitualmente para REST APIs, servicios GraphQL, servidores WebSocket en tiempo real, microservicios, herramientas CLI, proxies y API gateways. Su punto fuerte es el I/O no bloqueante: un proceso puede manejar muchas conexiones de red de forma eficiente. El trade-off es que cálculos CPU-heavy largos pueden bloquear el event loop si no se mueven a workers, procesos u otro servicio.
JavaScript se ejecuta en V8, mientras que las APIs de Node.js delegan trabajo asíncrono al sistema operativo o al thread pool de libuv. El hilo principal de JavaScript no espera a que termine una operación de I/O; continúa ejecutando otro código. Cuando la operación termina, su callback o continuación de Promise queda lista para el event loop. Así Node.js consigue alta concurrencia aunque el JavaScript de usuario normalmente se ejecute en un hilo principal.
V8 compila y ejecuta JavaScript y gestiona objetos y memoria. libuv proporciona el event loop cross-platform, integración con async I/O y un worker thread pool para operaciones como parte del filesystem, DNS y crypto. El event loop decide cuándo deben ejecutarse los callbacks de operaciones completadas. Node.js conecta las APIs JavaScript con capacidades nativas; V8 por sí solo no ofrece servidor HTTP, acceso a filesystem ni APIs de process.
Un proceso Node.js normalmente ejecuta JavaScript de usuario en un hilo principal, así que dos request handlers normales no pueden ejecutar JavaScript exactamente al mismo tiempo en ese hilo. Pero la plataforma no es literalmente de un solo hilo: libuv tiene thread pool, el sistema operativo realiza async I/O, V8 hace trabajo de garbage collection y Node.js también soporta worker_threads y child processes. La frase describe sobre todo el modelo de ejecución JavaScript por defecto.
5¿Cómo puede Node.js manejar muchas peticiones concurrentes?
Node.js inicia operaciones de I/O y vuelve al event loop en vez de bloquear un hilo por petición. Por ejemplo, 1000 llamadas a base de datos pueden estar in flight mientras el hilo principal sigue aceptando conexiones y procesando callbacks listos. Funciona bien para workloads I/O-bound como HTTP calls, consultas a base de datos, archivos y sockets. Funciona mal cuando los request handlers hacen trabajo CPU síncrono pesado, porque eso bloquea el event loop y retrasa todas las demás peticiones.
6¿Cuáles son las principales fases del bucle de eventos de Node.js?
Node.js utiliza fases de planificación de libuv como pending callbacks, idle/prepare, poll, check y close callbacks, junto con el procesamiento de temporizadores. Poll atiende muchas funciones de retorno de entrada/salida; check ejecuta setImmediate; los temporizadores ejecutan setTimeout/setInterval cuando se alcanza su umbral de demora y la planificación lo permite. En libuv 1.45 y Node.js 20+, se procesan después de poll en cada iteración, con una pasada inicial antes de entrar en el bucle por compatibilidad. process.nextTick es una cola independiente de Node, mientras que las reacciones de Promise y queueMicrotask utilizan la cola de microtareas de V8; ninguna de ellas es una fase de libuv. Se procesan en puntos de control entre funciones de retorno. Reponerlas recursivamente puede impedir que progrese otro trabajo.
7¿En qué se diferencian las operaciones síncronas, asíncronas y no bloqueantes?
Síncrono describe el flujo de control: una llamada produce su resultado o lanza una excepción antes de que continúe quien la invocó. Asíncrono describe un resultado entregado posteriormente mediante una función de retorno, Promise u otra notificación. Bloqueante describe una espera por trabajo o recursos durante la cual el hilo actual no puede hacer otra cosa; las API no bloqueantes evitan esa espera, por ejemplo devolviendo un resultado que indica que habría que esperar u organizando una notificación. Son dimensiones diferentes: síncrono no implica por sí solo esperar a que la entrada/salida esté lista, y devolver una Promise no convierte los cálculos síncronos pesados en no bloqueantes. En Node.js, fs.readFileSync es una API síncrona bloqueante; fs.promises.readFile entrega el resultado de forma asíncrona mientras el hilo principal puede realizar otro trabajo. En el procesamiento de solicitudes conviene usar API asíncronas de entrada/salida adecuadas.
La inanición del bucle de eventos ocurre cuando JavaScript síncrono o las colas de los puntos de control, repuestas continuamente, impiden procesar a tiempo las funciones de retorno de entrada/salida, los temporizadores y las solicitudes. Entre sus causas están los cálculos pesados, las operaciones con JSON de gran tamaño, las microtareas programadas recursivamente y el uso excesivo de process.nextTick. Limite el tamaño de entrada, perfile el trabajo costoso y traslade tareas intensivas a worker_threads u otro servicio. Al dividir el trabajo en fragmentos, ceda el control a otra vuelta del bucle, por ejemplo mediante setImmediate; esperar repetidamente Promises ya resueltas o programar queueMicrotask puede seguir privando de ejecución a la entrada/salida. Evite las API síncronas bloqueantes en rutas de solicitudes frecuentes.
9¿Qué son una función de retorno y el «infierno de callbacks»?
Una función de retorno se entrega a otra operación para que esta la invoque. Puede ejecutarse sincrónicamente, como en Array.map, o asíncronamente, como en fs.readFile. Las funciones de retorno asíncronas clásicas de Node.js suelen usar la convención de error como primer argumento: callback(error, result). El «infierno de callbacks» consiste en funciones dependientes profundamente anidadas que dificultan entender el flujo y gestionar errores. Las funciones con nombre, operaciones más pequeñas, Promises y async/await mejoran la legibilidad. Inicie juntas las operaciones independientes cuando corresponda, por ejemplo con Promise.all, respetando los límites de concurrencia y gestionando errores.
import fs from 'node:fs';
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
10¿Qué es una Promise y cómo funciona async/await?
Una Promise representa el resultado futuro de una operación asíncrona y puede estar pending, fulfilled o rejected. Una async function siempre devuelve una Promise. await pausa solo la async function actual hasta que la Promise se resuelve o rechaza; no bloquea todo el proceso Node.js. Una buena respuesta debe mencionar manejo de errores con try/catch, evitar Promises colgadas o no manejadas, y usar Promise.all para trabajo independiente para que las operaciones no se ejecuten innecesariamente en secuencia.
11¿En qué se diferencian process.nextTick, queueMicrotask, setImmediate y setTimeout?
process.nextTick programa trabajo en la cola next-tick de Node; queueMicrotask utiliza la cola de microtareas de V8, compartida con las reacciones de Promise. En un punto de control normal tras el código de nivel superior de CommonJS, nextTick se ejecuta antes que las funciones encoladas de Promise/queueMicrotask. En el nivel superior de un módulo ES, la evaluación ya ocurre en un contexto de microtarea, por lo que las microtareas encoladas pueden preceder a nextTick. Depende del contexto: no es una prioridad universal que interrumpa JavaScript en ejecución. setImmediate se ejecuta en check. setTimeout(fn, 0) espera un umbral mínimo y no es inmediato. Su orden relativo depende de dónde se programen; si ambos se programan dentro de una función de retorno de entrada/salida, setImmediate se ejecuta primero. Prefiera queueMicrotask para aplazar microtareas de forma portable, salvo que necesite el comportamiento específico de nextTick.
// CommonJS file (.cjs): nextTick precedes promise at this top level.
// ES module file (.mjs): promise precedes nextTick at this top level.
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// timeout vs immediate is context-dependent here.
Use try/catch para excepciones síncronas. En operaciones basadas en Promises, utilice await dentro de try/catch o un manejador de rechazo; un try/catch síncrono externo no captura una excepción lanzada posteriormente por una función de retorno. Las funciones con error como primer argumento deben comprobarlo, y los eventos error de EventEmitter necesitan manejadores adecuados. Los servidores deben centralizar los errores de solicitudes, registrar contexto útil y evitar exponer detalles internos al cliente. Tras uncaughtException, no suponga que el proceso puede continuar de forma segura: realice solo la limpieza síncrona de emergencia necesaria, termine y deje que un supervisor lo reinicie. Un cierre ordenado es útil en una terminación normal, pero la recuperación asíncrona arbitraria tras una excepción no capturada no es fiable.
Un módulo es una unidad aislada de código con una interfaz exportada explícita. Node.js soporta módulos built-in como node:fs y node:http, módulos locales del proyecto, paquetes npm, CommonJS modules y ECMAScript modules. Los módulos hacen más claros los límites de responsabilidad y permiten reutilización. El trade-off en producción es el coupling: si los módulos se importan de forma demasiado profunda o esconden side effects al importarse, las pruebas y el startup se vuelven más difíciles de razonar.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
14¿En qué se diferencian require de CommonJS e import de los módulos ES?
CommonJS utiliza require y module.exports; los módulos ES, import y export. require suele cargar sincrónicamente y puede invocarse de forma condicional. Las declaraciones import estáticas permiten analizar el grafo de dependencias; import() dinámico devuelve una Promise. Node.js moderno también puede cargar mediante require un grafo de módulos ES sin await de nivel superior, según las reglas de interoperabilidad admitidas: ESM no significa evaluación siempre asíncrona. Las extensiones .cjs/.mjs, el campo type de package.json y las reglas aplicables de detección sintáctica determinan el modo. ESM admite await de nivel superior. CommonJS ofrece __dirname/__filename; ESM en Node.js actual ofrece import.meta.dirname/filename para módulos de archivo, mientras import.meta.url sirve para resolver mediante URL y mantener compatibilidad con versiones anteriores.
// File: math.cjs (CommonJS)
function calculate(a, b) { return a + b; }
module.exports = { calculate };
// Separate file: math.mjs (ES module)
export function calculate(a, b) { return a + b; }
// These are two separate files, not one combined script.
npm es la interfaz de línea de comandos para gestionar paquetes. package.json describe metadatos, scripts y requisitos de dependencias directas, a menudo mediante rangos de versiones. package-lock.json registra un árbol resuelto con versiones e información de integridad. El archivo de bloqueo de una aplicación debe incluirse en el control de versiones. npm ci resulta útil en integración continua porque instala desde ese archivo, elimina un node_modules existente y falla si package.json y el archivo de bloqueo discrepan. El bloqueo reduce la deriva de dependencias, pero no iguala sistemas operativos, compilaciones nativas, scripts de ciclo de vida, versiones de Node ni opciones de instalación. Las compilaciones reproducibles también requieren herramientas y un entorno controlados.
16¿Cómo deben gestionarse la configuración y las variables de entorno?
Lea las variables mediante process.env: los valores presentes son cadenas y una propiedad ausente es undefined. Valide los valores obligatorios y conviértalos deliberadamente al iniciar; aplicar únicamente Number a PORT no rechaza una cadena vacía, NaN ni un puerto fuera de rango. Termine con un error claro si la configuración obligatoria no es válida. Mantenga los secretos reales fuera del control de versiones y suministre los de producción mediante la plataforma de despliegue o un gestor de secretos; sí puede incluir una configuración de ejemplo sin secretos. NODE_ENV es una convención de la aplicación, no una frontera de seguridad fiable.
const rawPort = process.env.PORT ?? '3000';
if (!/^\d+$/.test(rawPort)) throw new Error('PORT must be an integer');
const port = Number(rawPort);
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
throw new Error('PORT must be between 1 and 65535');
}
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl?.trim()) throw new Error('DATABASE_URL is required');
EventEmitter implementa un patrón publish-subscribe dentro de un proceso Node.js. on registra un listener, once registra un listener para un solo evento, off elimina un listener y emit llama síncronamente a los listeners en orden de registro. La naturaleza síncrona es importante: un listener lento bloquea el emitter. El evento error es especial porque un error event no manejado puede cerrar el proceso. Los sistemas de larga vida también deben eliminar listeners no usados para evitar memory leaks.
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
emitter.on('orderCreated', order => {
console.log(`Order ${order.id} created`);
});
emitter.emit('orderCreated', { id: 42 });
Buffer representa una secuencia de bytes de tamaño fijo. Se usa para archivos, TCP sockets, HTTP bodies, imágenes, criptografía y protocolos binarios. Buffer.from crea un buffer desde datos existentes, Buffer.alloc crea memoria inicializada a cero, y Buffer.allocUnsafe es más rápido pero puede contener datos residuales de memoria hasta sobrescribirse. Por eso allocUnsafe solo es apropiado cuando el código rellena inmediatamente el buffer completo y nunca expone stale data.
const buffer = Buffer.from('Hello', 'utf8');
console.log(buffer.toString('utf8'));
const safe = Buffer.alloc(1024);
// Prefer alloc for general use. Never expose allocUnsafe bytes before filling them.
const fast = Buffer.allocUnsafe(1024);
fast.fill(0); // All bytes initialized before use.
Los streams procesan datos por chunks en vez de cargar todo en memoria. Los tipos principales son Readable, Writable, Duplex y Transform. Archivos, objetos HTTP request y response, TCP sockets y gzip streams son ejemplos comunes. Los streams son valiosos para archivos grandes y tráfico de red porque reducen uso de memoria y latencia. Una respuesta de producción debe mencionar que los errores de stream deben manejarse correctamente en toda la cadena.
20¿Qué es backpressure y por qué es útil pipeline?
Backpressure ocurre cuando una fuente produce chunks más rápido de lo que el destino puede consumirlos. Sin backpressure, la memoria puede crecer hasta que el proceso se ralentiza o cae. Streams y pipe pueden coordinar el flujo, pero pipeline de node:stream/promises es más seguro para cadenas de varios pasos porque propaga errores, cierra streams relacionados y devuelve una Promise. Es un buen default para compresión de archivos, uploads, downloads y transformaciones de streams.
import fs from 'node:fs';
import zlib from 'node:zlib';
import { pipeline } from 'node:stream/promises';
await pipeline(
fs.createReadStream('input.txt'),
zlib.createGzip(),
fs.createWriteStream('input.txt.gz')
);