Tres horas. Media hora para entender qué desaparece y qué aparece al sacar JavaScript del navegador, y dos horas y media montando un proyecto de Node con su configuración fuera del código y sus responsabilidades separadas.
Hoy · Hoja de ruta
- 1. Aprende: Qué es Node.js, qué desaparece y qué aparece respecto al navegador, cómo recibe datos un programa de terminal y cómo se organiza un proyecto por responsabilidades.
- 2. Haz: Ejecuta tus primeros programas, escribe uno configurable desde el entorno y estructura el proyecto que usarás toda la unidad.
- 3. Comprueba: La lógica de la UD3 funciona aquí sin tocar una coma, y ninguna clave está escrita en el código.
Antes de empezar · 5 minutos, sin apuntes
- ¿Qué partes de tu código de la UD4 crees que no funcionarán fuera del navegador? ¿Y cuáles funcionarán sin tocar nada?
- ¿Dónde guardarías una contraseña de base de datos?
- ¿Cómo sabe otro programa si el tuyo ha terminado bien?
Se explica
25 minutos · conceptos y demostración
El lenguaje es el mismo; cambia todo lo que lo rodea. Hoy se inventaría qué se pierde, qué se gana, y por qué la parte de tu código que no sabía nada de la página es la única que viaja intacta.
Un motor sin navegador
Node.js es el motor de JavaScript de Chrome, sacado del navegador y empaquetado como un programa que puedes ejecutar en tu máquina. Alrededor le añadieron lo que un navegador no le deja hacer: acceso al sistema de ficheros, a la red y a los procesos del sistema.
node --version
node -e "console.log('Hola desde Node')"
node src/hola.js
document.querySelector("h1"); // ReferenceError: document is not defined
window.localStorage; // ReferenceError
alert("Hola"); // ReferenceError
No están rotos: no tienen sentido. No hay documento ni ventana. Todo el módulo catalogo.js de la UD3, en cambio, funciona aquí sin cambiar una coma, porque solo trabaja con datos. Ese es, otra vez, el premio de haber separado la lógica de la página.
process.argv; // los argumentos de la llamada
process.env; // las variables de entorno
process.exit(1); // terminar con un código
process.cwd(); // desde dónde se ejecutó
Los módulos del sistema se importan con el prefijo node::
import { readFile } from "node:fs/promises";
import path from "node:path";
import http from "node:http";
El prefijo node:
Deja claro que el módulo es del propio Node y no una dependencia descargada. Sin él también funciona, pero con él nadie puede confundir tu importación con un paquete externo instalado por alguien.
Un programa de Node no es una página
Una página vive mientras esté abierta y reacciona a lo que hace una persona. Un programa de Node se ejecuta, hace su trabajo y termina; salvo que sea un servidor, y entonces se queda escuchando hasta que alguien lo pare.
De ahí una diferencia práctica: un fallo en el navegador estropea la experiencia de quien visita la web; un fallo no capturado en un servidor puede tumbarlo para todo el mundo a la vez.
Cómo entran los datos
node src/cli.js listar
node src/cli.js buscar teclado
node src/cli.js añadir "Ratón vertical" 39.9
const [, , comando, ...argumentos] = process.argv;
console.log(comando); // "buscar"
console.log(argumentos); // ["teclado"]
Las dos primeras posiciones son siempre el ejecutable y el fichero, por eso se descartan. Y, como todo lo que entra desde fuera, los argumentos son texto: un precio hay que convertirlo y comprobarlo, igual que hacías con los campos de formulario.
La configuración, en cambio, no entra por argumentos sino por el entorno:
const puerto = Number(process.env.PUERTO ?? 3000);
const entorno = process.env.NODE_ENV ?? "development";
Lo que cambia entre máquinas va en el entorno
El puerto, la ruta de los datos, la dirección de la base de datos y cualquier clave. Van fuera del código porque cambian entre tu portátil, el aula y el servidor real, y porque una clave escrita en el código acaba, tarde o temprano, en un repositorio.
Nota además el ?? 3000: un valor por defecto razonable para que el programa arranque sin configurar nada. El que no debe tener valor por defecto es un secreto: si falta, el programa debe negarse a arrancar.
En cuanto haya más de una variable, se pasan por fichero. Desde Node 20 se puede cargar un .env sin instalar nada, y la orden es idéntica en las tres terminales:
node --env-file=.env src/servidor.js
Al repositorio se incorpora .env.example, con las claves y sin los valores. El .env real va al .gitignore.
Cómo sale el resultado
console.log("Resultado"); // salida estándar
console.error("Ha fallado"); // salida de error
process.exit(0); // todo bien
process.exit(1); // ha fallado
Código de salida
El número con el que termina un programa. Cero significa éxito; cualquier otro, fallo. Es como se enteran las herramientas que encadenan programas —un script, un sistema de integración continua— de si deben seguir o pararse.
Separar console.log de console.error importa por lo mismo: permite que quien use tu programa redirija los resultados a un fichero y siga viendo los errores.
Dos sistemas de módulos
| Sistema | Cómo se escribe | Cuándo aparece |
|---|---|---|
| ESM | import / export |
El nuestro, y el estándar del lenguaje |
| CommonJS | require / module.exports |
Node antiguo y mucha documentación |
Para usar ESM basta una línea en package.json:
{ "type": "module" }
Verás muchísimo require por ahí. Funciona, pero es el sistema heredado: si una respuesta de IA o un tutorial te lo dan, tradúcelo en lugar de mezclar los dos.
Dos diferencias prácticas. En ESM no existen __dirname ni __filename, y la extensión es obligatoria en las rutas relativas, igual que en el navegador:
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
const aqui = dirname(fileURLToPath(import.meta.url));
Estructura por responsabilidad
- almacen.js · dónde están los datos
- catalogo.js · qué reglas tienen
- servidor.js · cómo se piden desde fuera
- cli.js · cómo se piden desde la terminal
La lógica no sabe de dónde vienen los datos
Tus funciones de catálogo reciben un array y devuelven otro. No leen ficheros, no conocen HTTP y no imprimen nada. Por eso las puedes usar hoy desde la terminal y mañana desde el servidor sin tocarlas.
Esa separación es la que hace que el cambio de la UD6 —pasar del fichero a otra forma de guardar— afecte a un solo módulo. Es también, exactamente, la arquitectura por capas del módulo de servidor.
Se trabaja
150 minutos · montar el proyecto de la unidad
Lo que montes hoy es el esqueleto sobre el que trabajarás las seis sesiones. Los dos últimos pasos comprueban que la separación que has hecho es real y no solo una disposición de carpetas.
Paso 1 · Primeros programas · 30 min
- Comprueba tu versión de Node; debe ser 22 o superior.
- Crea
src/hola.jsque salude e imprima la versión desdeprocess.version. - Copia tu
catalogo.jsde la UD3 y ejecuta sus funciones desde Node. - Intenta usar
documenty anota el error exacto. - Imprime
process.cwd()ejecutando desde dos carpetas distintas y explica la diferencia. - Prueba el modo interactivo —la orden
nodesin argumentos— con tres expresiones.
Antes de continuar: tus funciones de la UD3 devuelven lo mismo aquí que devolvían en el navegador, y no has tenido que cambiar ni una línea.
Paso 2 · Un programa configurable · 40 min
- Escribe
src/cli.jsque acepte los comandoslistar,buscaryayuda. - Si no se pasa comando, muestra la ayuda y termina con código 1.
- Lee de una variable de entorno la ruta del fichero de datos, con un valor por defecto.
- Convierte y valida los argumentos numéricos.
- Comprueba el código de salida tras cada ejecución. En Git Bash es
echo $?; en PowerShell,$LASTEXITCODE. - Crea
.env.exampley añade.enval.gitignore.
Estoy atascado · la variable de entorno no llega
La forma de asignarla depende de la terminal, y es de las cosas que más tiempo hacen perder:
PUERTO=4000 node src/servidor.js Git Bash, macOS, Linux
$env:PUERTO="4000"; node src/servidor.js PowerShell
set PUERTO=4000 && node src/servidor.js cmd de Windows
Si tienes dudas sobre cuál estás usando, imprime process.env entero y mira si tu variable aparece. En cuanto haya más de una, pasa al fichero .env, cuya orden sí es idéntica en las tres.
Paso 3 · Estructurar el proyecto · 40 min
Es el trabajo central de la sesión.
- Crea la carpeta
mi-api/conpackage.jsony"type": "module". - Crea
src/conalmacen.js,catalogo.jsycli.js. - Mueve ahí tus funciones de la UD3 sin modificarlas.
- Escribe en un comentario, en cada fichero, la frase que lo describe. Si necesitas dos frases, el fichero hace dos cosas.
- Calcula la ruta de
datos/productos.jsona partir de la posición del fichero, no de dónde se ejecute. - Comprueba que
cli.jsfunciona llamándolo desde otra carpeta distinta.
El apartado 6 es el que delata el error clásico: una ruta relativa al directorio de trabajo funciona mientras ejecutes desde la raíz del proyecto y falla en cuanto alguien lo llama desde otro sitio.
Paso 4 · Qué sobrevive de la UD4 · 20 min
Recorre tu proyecto del navegador y clasifica cada módulo:
| Módulo de la UD4 | ¿Funciona en Node? | Por qué | Qué habría que cambiar |
|---|---|---|---|
datos.js |
|||
catalogo.js |
|||
formato.js |
|||
render.js |
|||
dom.js |
|||
estado.js |
|||
main.js |
Cuenta cuántos pasan intactos. Ese número es la medida de lo bien separado que estaba tu código, y no depende de lo listo que fuera: depende de si cada módulo sabía solo de lo suyo.
Los dos casos interesantes son formato.js y estado.js. Míralos con cuidado antes de decidir.
Paso 5 · El grafo de dependencias · 20 min
Dibuja qué módulo importa a cuál, con una flecha por import.
- ¿Alguna flecha va de
catalogo.jshaciaalmacen.js? Si la hay, tu lógica sabe de dónde vienen los datos, y eso es lo que hoy queremos evitar. - ¿Hay ciclos?
- ¿Qué módulo es el único que conoce el sistema de ficheros?
- Imagina que mañana los datos vienen de una base de datos. Marca en el dibujo todos los ficheros que habría que tocar. Si es más de uno, señala por qué.
- Guarda el dibujo: en la sesión 5 lo compararás con el del servidor ya montado.
Ampliación si has completado el trabajo
Primero termina y comprueba los cinco pasos. El primer reto trata la configuración como lo que es: una parte del programa que también falla.
Reto 1 · El programa que se niega a arrancar
Un servidor que arranca sin su clave de base de datos no está funcionando: está esperando a fallar en la primera petición, en producción y de madrugada.
- Escribe un módulo
config.jsque lea todas las variables de entorno en un solo sitio y las exporte ya convertidas y validadas. - Distingue tres clases de variable: las que tienen un valor por defecto razonable, las obligatorias sin valor por defecto, y las opcionales que pueden faltar.
- Si falta una obligatoria, el programa debe terminar antes de hacer nada, con un mensaje que diga qué variable falta y cómo se define. Usa el código de salida adecuado.
- Valida también los tipos: un puerto que llega como
"abc"debe fallar igual de pronto que uno que falta. - Comprueba tu
config.jsen los cuatro escenarios: todo definido, sin la obligatoria, con el puerto no numérico, y con una variable opcional ausente. - Escribe en tres líneas por qué esta comprobación va en el arranque y no en el punto donde se usa cada variable. La respuesta conecta con «validar en el borde» de la UD3.
Reto 2 · Traducir de CommonJS
Este fragmento es el que te va a dar cualquier tutorial antiguo o cualquier respuesta de IA que no le hayas acotado la versión.
const fs = require("fs");
const path = require("path");
const { buscar } = require("./catalogo");
const RUTA = path.join(__dirname, "..", "datos", "productos.json");
function cargar() {
const texto = fs.readFileSync(RUTA, "utf8");
return JSON.parse(texto);
}
module.exports = { cargar, RUTA };
- Tradúcelo a ESM. Hay cuatro cosas que cambiar, y una de ellas no es una traducción directa:
__dirnameno existe. - La función usa la versión síncrona de la lectura. Averigua qué significa eso en un servidor y reescríbela con la API de promesas. Explica en dos líneas por qué en un programa de terminal daba igual y en un servidor no.
- Comprueba qué pasa si dejas
requireen un fichero de un proyecto con"type": "module". Anota el error exacto: lo vas a ver más de una vez. - Comprueba también el caso inverso: un
importen un proyecto sin"type": "module". - Explica en tres líneas por qué no conviene mezclar los dos sistemas en un mismo proyecto, aunque técnicamente haya formas de hacerlo convivir.
config.js que se niega a arrancar probado en cuatro escenarios, o el fragmento de CommonJS traducido y reescrito con promesas.Cierre
5 minutos · comprobación y recuerdo
Lista de verificación de la sesión
- Ejecutas programas desde la terminal y les pasas datos.
- Reutilizas sin cambios la lógica de la UD3.
- Importas módulos del sistema con el prefijo
node:. - La configuración vive en el entorno, y
.envno está en el repositorio. - Terminas con el código de salida adecuado y separas salida de error.
- Tu proyecto usa ESM y cada fichero se explica en una frase.
Antes de cerrar · 3 minutos, sin mirar
- ¿Por qué no existe
documenten Node? - ¿Qué indica el prefijo
node:en una importación? - ¿Por qué se descartan las dos primeras posiciones de
process.argv? - ¿Qué significa un código de salida distinto de cero?
- ¿Qué se sube al repositorio:
.envo.env.example? - ¿Cómo se activa ESM en un proyecto de Node?
Ver respuestas
1 · Porque no hay documento: Node ejecuta JavaScript sin navegador.
2 · Que el módulo es del propio Node y no una dependencia instalada.
3 · Porque son el ejecutable de Node y la ruta del fichero ejecutado.
4 · Que el programa ha terminado con un fallo.
5 · Solo el de ejemplo, con las claves y sin los valores reales.
6 · Añadiendo "type": "module" al package.json.
Microprueba semanal 1 · 5–10 minutos
Individual, sin IA y sin apuntes.
- Nombra dos cosas que existen en el navegador y no en Node, y dos que existen en Node y no en el navegador.
- ¿Qué significa que un programa termine con código de salida 1?
- ¿Dónde vive el puerto de tu servidor, y por qué no está escrito en el código?