Lección 1 de 313 min
Node.js: JavaScript en el servidor
V8, el event loop y la E/S no bloqueante, CommonJS frente a ESM, los módulos integrados y los streams.
Durante años, JavaScript solo vivía en el navegador. En 2009, Ryan Dahl sacó el motor de JavaScript de Chrome y lo combinó con acceso a archivos, a la red y al sistema operativo. Así nació Node.js, y con él la posibilidad de escribir el frontend y el backend en el mismo lenguaje.
Hoy Node ejecuta APIs y herramientas de línea de comandos, y la mayoría de las herramientas de build que viste en el módulo anterior se instalan y se lanzan con él. En esta lección verás qué hay dentro, por qué puede atender miles de conexiones con un solo hilo y qué piezas usarás en cualquier proyecto: módulos, streams, process y variables de entorno.
Cómo funciona Node
V8 y libuv
Un motor de JavaScript y una librería de E/S asíncrona, unidos en un solo programa.
- Creado por:
- Ryan Dahl
- Desde:
- 2009
- Motor:
- V8
Node.js no es un lenguaje ni un framework: es un runtime, un programa que ejecuta JavaScript fuera del navegador. Por dentro tiene dos piezas principales. V8, el motor de JavaScript de Chrome, compila tu código a código máquina. libuv, una librería escrita en C, se encarga de todo lo que implica esperar: leer archivos, hablar por la red, temporizadores.
Lo que no tiene Node es lo que da el navegador: no hay document, ni window, ni DOM. A cambio, tiene acceso al sistema de archivos, puede abrir puertos y lanzar procesos. Muchas APIs web sí existen en Node, como fetch, URL o crypto.randomUUID(), así que mucho código funciona igual en los dos sitios.
Úsalo cuando
Para APIs y servidores web, tareas en tiempo real (chats, notificaciones), herramientas de línea de comandos y scripts. Es especialmente bueno cuando el trabajo consiste sobre todo en esperar a bases de datos y a otras APIs.
Ten en cuenta
No es la mejor opción para cálculos intensivos, como procesar vídeo o entrenar modelos: bloquean el hilo principal. Existen alternativas con APIs parecidas, como Deno y Bun, pero Node sigue siendo el más extendido.
$ node --version
v24.11.0
$ node -e "console.log(typeof window, typeof process)"
undefined object
$ node servidor.js # ejecuta un archivo
$ node # abre la consola interactiva (REPL)El event loop y la E/S no bloqueante
Un solo hilo que nunca espera: delega la espera y sigue con lo siguiente.
- Hilos para tu código:
- 1
- Lo implementa:
- libuv
Tu código JavaScript se ejecuta en un solo hilo. Cuando pides algo lento, como leer un archivo o consultar una base de datos, Node no se queda esperando: se lo encarga al sistema operativo (o a un pequeño grupo de hilos de libuv) y sigue atendiendo otras peticiones. Cuando la respuesta llega, el event loop («bucle de eventos») pone en cola la función que debe continuar el trabajo.
Piensa en un camarero que toma el pedido de una mesa, lo pasa a cocina y atiende la siguiente mesa en lugar de quedarse mirando el horno. Por eso un solo proceso de Node atiende miles de conexiones a la vez: la mayor parte del tiempo, cada petición está esperando, no calculando.
Úsalo cuando
Entenderlo te sirve siempre: explica por qué usas await con todo lo que implica E/S y por qué un cálculo pesado deja colgado todo el servidor.
Ten en cuenta
Si tu código calcula durante mucho tiempo (un bucle enorme, un JSON.parse de cientos de megas, readFileSync en cada petición), el hilo está ocupado y nadie más es atendido. Para trabajo de CPU, usa worker_threads o llévalo a otro servicio.
import { readFile } from "node:fs/promises";
console.log("1. Pido el archivo al sistema operativo");
readFile("pedidos.json", "utf8").then((texto) => {
console.log("3. Ya llegó:", texto.length, "caracteres");
});
console.log("2. Mientras tanto, sigo con lo demás");
// Mal en un servidor: bloquea el hilo hasta que termina
// const texto = readFileSync("pedidos.json", "utf8");¿Sabías que…?
«Un solo hilo» se refiere a tu JavaScript. Node, por dentro, usa más hilos: libuv mantiene un pequeño grupo para tareas que el sistema operativo no sabe hacer de forma asíncrona, como ciertas operaciones de archivos o de criptografía.
Módulos y streams
CommonJS y módulos ES
Dos sistemas de módulos: el histórico de Node (require) y el estándar (import).
- CommonJS:
- require
- ESM:
- import
Node nació antes de que JavaScript tuviera módulos, así que inventó los suyos: CommonJS, con require() y module.exports. Años después, el lenguaje estandarizó los módulos ES (ESM), con import y export, que son los que usa el navegador y el formato recomendado hoy.
Node decide cuál usar por la extensión: .mjs es ESM y .cjs es CommonJS. Para los .js, mira el campo type del package.json más cercano: con "type": "module", son ESM. Solo en ESM tienes await en el nivel superior del archivo; a cambio, no existen __dirname ni require, y en su lugar usas import.meta.dirname e import.
Úsalo cuando
Usa ESM en proyectos nuevos. Encontrarás CommonJS en código y librerías antiguas, y en archivos de configuración como webpack.config.js.
Ten en cuenta
Mezclarlos da errores como «require is not defined in ES module scope». Desde ESM puedes importar paquetes CommonJS sin problema; en las versiones recientes, require() también puede cargar módulos ESM, siempre que no usen await en el nivel superior.
// precio.cjs (CommonJS)
const { redondear } = require("./utilidades.cjs");
module.exports = { conIva: (p) => redondear(p * 1.21) };
// precio.mjs (módulos ES)
import { redondear } from "./utilidades.mjs";
export const conIva = (p) => redondear(p * 1.21);
// Solo en ESM: await en el nivel superior
const config = await import("./config.mjs");
console.log(import.meta.dirname); // en lugar de __dirnameMódulos integrados
Archivos, rutas, HTTP y criptografía sin instalar nada, con el prefijo node:.
- Prefijo:
- node:
Node trae de serie una librería estándar. Los módulos que más usarás son node:fs/promises para leer y escribir archivos con promesas, node:path para construir rutas que funcionen en cualquier sistema operativo, node:http para crear servidores y hacer peticiones, y node:crypto para hashes, identificadores aleatorios y cifrado.
El prefijo node: deja claro que es un módulo integrado y no un paquete de npm con el mismo nombre. Es opcional en los módulos clásicos (fs también funciona), pero algunos módulos nuevos, como node:test, solo existen con prefijo.
Úsalo cuando
Antes de instalar un paquete, comprueba si Node ya lo hace: generar un UUID, hacer un fetch, ejecutar tests o leer argumentos de la terminal ya vienen incluidos.
Ten en cuenta
Construye las rutas con path.join en lugar de juntar textos con /: en Windows el separador es \. Y en un servidor, usa siempre las versiones asíncronas; las que acaban en Sync bloquean el event loop.
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { randomUUID } from "node:crypto";
const ruta = path.join(import.meta.dirname, "datos", "recetas.json");
createServer(async (req, res) => {
const recetas = await readFile(ruta, "utf8");
res.writeHead(200, {
"Content-Type": "application/json",
"X-Request-Id": randomUUID(),
});
res.end(recetas);
}).listen(3000);Streams
Procesar datos trozo a trozo, sin cargarlos enteros en memoria.
- Tipos:
- Readable, Writable, Transform
Un stream es un flujo de datos que llega por partes. En lugar de leer un archivo de 5 GB entero en memoria y luego procesarlo, lo lees en trozos pequeños y cada uno se procesa y se envía en cuanto llega. Hay streams de lectura (Readable), de escritura (Writable) y de transformación (Transform), que reciben datos, los modifican y los pasan.
Están por todas partes en Node: la petición HTTP (req) es un stream de lectura y la respuesta (res), uno de escritura. pipeline los conecta en cadena y gestiona la contrapresión (backpressure): si el destino es más lento que el origen, pausa la lectura para no llenar la memoria.
Úsalo cuando
Con archivos grandes, exportaciones a CSV, subidas y descargas, compresión o cualquier dato que no conviene tener entero en memoria.
Ten en cuenta
Conectar streams con .pipe() no propaga los errores ni cierra los demás streams si uno falla. Usa pipeline de node:stream/promises, que sí lo hace y además se puede usar con await.
import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { createGzip } from "node:zlib";
// Comprime un registro enorme sin cargarlo entero en memoria
await pipeline(
createReadStream("acceso.log"),
createGzip(),
createWriteStream("acceso.log.gz"),
);
console.log("Comprimido");El proceso y su entorno
process
El objeto global con todo sobre el programa en marcha: argumentos, entorno y señales.
- Tipo:
- Objeto global
process está disponible en cualquier archivo sin importarlo. Con process.argv lees los argumentos de la terminal, con process.cwd() sabes desde qué carpeta se lanzó el programa, process.env contiene las variables de entorno y process.exitCode indica con qué código terminará (0 es «todo bien»; cualquier otro, error).
También te avisa de lo que le pasa al programa desde fuera: process.on("SIGTERM", …) se ejecuta cuando Docker, Kubernetes o tu plataforma piden al proceso que se detenga. Es el momento de dejar de aceptar peticiones y cerrar las conexiones a la base de datos con calma.
Úsalo cuando
En scripts de línea de comandos (argumentos y código de salida) y en cualquier servidor que deba apagarse sin cortar peticiones a medias.
Ten en cuenta
process.exit() termina de inmediato, aunque queden escrituras pendientes en un archivo o en los logs. Si solo quieres indicar un error, asigna process.exitCode = 1 y deja que el programa termine solo.
import { createServer } from "node:http";
// node app.js --puerto 4000
console.log(process.argv.slice(2)); // [ '--puerto', '4000' ]
console.log(process.cwd()); // carpeta desde la que se lanzó
console.log(process.version); // versión de Node
const servidor = createServer(atenderPeticion).listen(3000);
// La plataforma pide parar: deja de aceptar conexiones
// nuevas y termina las que están en curso
process.on("SIGTERM", () => {
servidor.close(() => console.log("Cerrado sin cortar peticiones"));
});Variables de entorno y --env-file
Configuración y secretos fuera del código, distintos en cada entorno.
- Se leen con:
- process.env
Las variables de entorno son pares nombre-valor que el sistema pasa a cada programa al arrancarlo. Son el lugar estándar para la configuración que cambia entre entornos (el puerto, la URL de la base de datos) y para los secretos (claves de API, contraseñas): el código es el mismo en desarrollo y en producción, y solo cambian las variables.
En desarrollo, lo cómodo es guardarlas en un archivo .env. Durante años hizo falta el paquete dotenv para leerlo; las versiones actuales de Node lo hacen solas con node --env-file=.env app.js.
Úsalo cuando
Para todo lo que cambia entre entornos y, sobre todo, para cualquier secreto. En producción, las variables las define tu plataforma (Docker, Kubernetes, el panel de tu proveedor), no un archivo.
Ten en cuenta
Nunca subas el .env a Git: añádelo a .gitignore y sube en su lugar un .env.example con los nombres y sin valores. Los valores siempre llegan como texto: process.env.PUERTO + 1 con PUERTO=4000 da "40001".
# .env (no lo subas a Git)
PUERTO=4000
DATABASE_URL=postgres://localhost:5432/tienda
// app.js
const puerto = Number(process.env.PUERTO ?? 3000); // llega como texto
if (!process.env.DATABASE_URL) {
throw new Error("Falta la variable DATABASE_URL");
}
# En la terminal:
# node --env-file=.env app.jsVersiones LTS
Las versiones con soporte largo, las que debes usar en producción.
- Sigla:
- Long Term Support
Node publica versiones mayores con regularidad. Algunas pasan a ser LTS (Long Term Support, «soporte a largo plazo»): durante años reciben correcciones de errores y de seguridad, sin cambios que rompan tu código. Tradicionalmente, las versiones pares (20, 22, 24…) se convierten en LTS y las impares duran poco.
Cuando una versión llega a su fin de vida (EOL), deja de recibir parches de seguridad, aunque siga funcionando. El calendario oficial indica en qué fase está cada versión: «Current» (la más nueva), «Active LTS», «Maintenance LTS» o fin de vida.
Úsalo cuando
En producción, usa siempre una versión LTS en soporte activo o de mantenimiento. Fija la versión del proyecto en .nvmrc y en el campo engines de package.json para que todo el equipo y el CI usen la misma.
Ten en cuenta
No te quedes en una versión en fin de vida «porque funciona»: es un riesgo de seguridad. Consulta siempre el calendario oficial antes de elegir, porque el proyecto puede ajustarlo.
# .nvmrc
24
# Con un gestor de versiones como nvm o fnm:
nvm install # instala la versión de .nvmrc
nvm use # y la activa en esta terminal
// package.json
{
"engines": { "node": ">=24" }
}Atención
Lo que va en el código va a Git, y lo que va a Git lo puede leer cualquiera con acceso al repositorio, incluso años después. Por eso los secretos van en variables de entorno, nunca escritos en un archivo .js. Lo verás a fondo en el módulo de seguridad.
Interactivo
CommonJS y módulos ES
Activa o desactiva opciones para compararlas cara a cara.
| Característica | CommonJS | Módulos ES |
|---|---|---|
| Sintaxis | require() y module.exports | import y export |
| Cómo lo activa Node | Extensión .cjs, o .js sin "type": "module" | Extensión .mjs, o .js con "type": "module" |
await en el nivel superior | No | Sí |
| Carpeta del archivo | __dirname | import.meta.dirname |
| Dónde funciona | Solo en Node (y en bundlers) | Node, navegadores, Deno, Bun y Workers |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Tu API va bien hasta que alguien pide el informe anual: mientras se genera, calculado en un bucle enorme, todas las demás peticiones se quedan esperando.
Para recordar
- Node.js es un runtime: V8 ejecuta tu JavaScript y libuv gestiona la E/S asíncrona.
- Tu código corre en un solo hilo; Node atiende a muchos a la vez porque delega las esperas, así que nunca bloquees el event loop con cálculos largos ni funciones
Sync. - Usa módulos ES (
import) en proyectos nuevos; CommonJS (require) sigue presente en código antiguo. - Para datos grandes, streams con
pipeline; para configuración y secretos, variables de entorno con--env-fileen local. - En producción, siempre una versión LTS con soporte.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.Un endpoint de tu API calcula durante tres segundos un informe en un bucle. ¿Qué les pasa al resto de usuarios durante ese tiempo?
2.En el ejemplo de
orden.js, conreadFile(...).then(...)entre dosconsole.log, ¿en qué orden aparecen los mensajes?3.¿Qué cambia si añades
"type": "module"a tupackage.json?4.Con
PUERTO=4000en tu.env, ¿qué devuelveprocess.env.PUERTO + 1?5.¿Qué ventaja tiene comprimir un archivo de 5 GB con streams en lugar de leerlo entero y comprimirlo?