Cerrábamos el Módulo 3 con una frase: el proyecto sabe leer y escribir, pero no sabe hablar con nadie. Escena Viva tiene un catálogo en disco, informes por mes, rutas blindadas y tuberías que procesan ventas con memoria constante. Todo eso lo consume una única persona: quien ejecuta el comando en su terminal.
Hoy eso cambia. Al terminar esta lección, cualquier navegador de tu red podrá pedirle datos a Escena Viva.
Y verás enseguida que no estamos en territorio desconocido. Un servidor HTTP de Node es un EventEmitter que emite request —los mismos on y emit de la lección 02-05—, arrancarlo es una operación asíncrona que se confirma con un evento, y mantener el proceso vivo es exactamente el mecanismo de cuenta de referencias del bucle de eventos que estudiamos en 02-01. El módulo node:http no añade conceptos nuevos: conecta a la red los que ya tienes.
Contenido
- HTTP en cinco minutos: petición, respuesta y un intercambio real
http.createServer: el servidor es unEventEmitterserver.listen, el eventolisteningy el puerto- El primer servidor de Escena Viva
- Tres formas de probarlo: navegador,
curlynode -e - Por qué el proceso ya no termina solo
- Errores de arranque:
EADDRINUSEyEACCES - Apagado ordenado:
server.close()ySIGINT
- HTTP en cinco minutos: petición, respuesta y un intercambio real
HTTP es un protocolo de petición y respuesta sobre texto. El cliente abre una conexión TCP, envía un bloque de texto con un formato muy concreto, y el servidor contesta con otro bloque. Nada más. Ni el cliente ni el servidor mantienen memoria de lo ocurrido entre una petición y otra: HTTP es sin estado, y todo lo que parece estado (sesiones, carritos, usuarios conectados) se construye encima, como veremos en el Módulo 8.
Una petición tiene cuatro partes y una respuesta tres:
| Mensaje | Parte | Ejemplo | Qué significa |
|---|---|---|---|
| Petición | Método | GET, POST, PUT, DELETE, HEAD |
La intención: leer, crear, reemplazar, borrar |
| Petición | Ruta | /eventos/evt-001?formato=json |
Qué recurso, con parámetros de consulta opcionales |
| Petición | Cabeceras | Accept: application/json |
Metadatos: qué acepto, quién soy, qué envío |
| Petición | Cuerpo | {"sesionId":"ses-001-1"} |
Datos. Opcional; GET normalmente no lo lleva |
| Respuesta | Código de estado | 200, 404, 500 |
Cómo ha ido, en un número de tres cifras |
| Respuesta | Cabeceras | Content-Type: application/json |
Metadatos sobre lo que devuelvo |
| Respuesta | Cuerpo | El JSON, el HTML, los bytes del cartel | El contenido. Opcional (un 204 no lleva) |
Así se ve un intercambio completo, tal cual viaja por el cable. La línea en blanco es lo que separa las cabeceras del cuerpo, y es obligatoria:
GET /eventos HTTP/1.1
Host: localhost:3000
User-Agent: curl/8.5.0
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 74
Date: Tue, 14 Aug 2026 09:12:30 GMT
Connection: keep-alive
{
"eventos": 3,
"sesiones": 7,
"entradasLibres": 1189
}Fíjate en dos detalles que te van a ahorrar horas de depuración más adelante:
- La ruta que llega al servidor no incluye el dominio.
Host: localhost:3000viaja en una cabecera aparte. Por eso, como veremos en la próxima lección,req.urlvale/eventosy nuncahttp://localhost:3000/eventos. - Las cabeceras son texto plano
Nombre: valor, insensibles a mayúsculas, y pueden repetirse. Node te las entregará siempre normalizadas a minúsculas.
La buena noticia es que no vas a escribir ese texto a mano: el módulo node:http lo analiza al recibirlo y lo genera al responder. Tú trabajas con dos objetos, req y res.
http.createServer: el servidor es un EventEmitter
http.createServer: el servidor es un EventEmitterEl servidor mínimo cabe en cuatro líneas:
const http = require('node:http');
const servidor = http.createServer((req, res) => {
res.end('Hola desde Escena Viva\n');
});
servidor.listen(3000);Guárdalo como hola.js, ejecútalo con node hola.js y abre http://localhost:3000. Ya tienes un servidor web.
Ahora lo importante: esa función que le pasas a createServer no es nada especial. http.Server hereda de net.Server, que hereda de EventEmitter. Pasar el manejador al constructor es puro azúcar sintáctico: Node hace un servidor.on('request', manejador) por ti. Estas dos versiones son exactamente equivalentes:
// Version A: el manejador como argumento (la habitual)
const servidor = http.createServer((req, res) => {
res.end('hola');
});
// Version B: registrando el oyente a mano (identica a la anterior)
const servidor = http.createServer();
servidor.on('request', (req, res) => {
res.end('hola');
});Saber esto no es trivia: significa que puedes registrar varios oyentes de request, y que todo lo que aprendiste sobre EventEmitter aplica aquí. Un caso real es separar el registro de peticiones de la lógica:
// Se ejecuta antes que el otro: los oyentes se invocan en orden de registro.
servidor.on('request', (req) => console.error(`[peticion] ${req.method} ${req.url}`));
servidor.on('request', manejarPeticion); // La logica de verdadEl diagnóstico va a stderr y los datos a stdout, como venimos haciendo desde el Módulo 1.
Estos son los eventos del servidor que importan en este módulo:
| Evento | Cuándo se emite | Argumentos |
|---|---|---|
request |
Llega una petición HTTP completa (cabeceras; el cuerpo aún puede estar viajando) | (req, res) |
listening |
El socket ya está aceptando conexiones | — |
connection |
Se abre una conexión TCP, antes de cualquier petición | (socket) |
close |
El servidor ha dejado de aceptar y ya no quedan conexiones | — |
error |
Fallo del servidor, típicamente al arrancar | (error) |
clientError |
Un cliente ha enviado algo que no es HTTP válido | (error, socket) |
La diferencia entre connection y request merece un momento. No son lo uno por lo otro: HTTP/1.1 mantiene la conexión abierta por defecto (Connection: keep-alive), de modo que un navegador que carga una página con tres imágenes puede abrir una conexión y enviar cuatro peticiones por ella. Verlo en directo aclara el concepto:
let conexiones = 0;
let peticiones = 0;
servidor.on('connection', () => console.error(`[tcp] conexiones: ${++conexiones}`));
servidor.on('request', () => console.error(`[http] peticiones: ${++peticiones}`));Recarga la página un par de veces y verás cómo el contador de peticiones sube mucho más deprisa que el de conexiones. Ese detalle vuelve en el apartado 8, cuando intentemos apagar el servidor.
server.listen, el evento listening y el puerto
server.listen, el evento listening y el puertolisten es lo que pone al servidor a escuchar. Su firma habitual es listen(puerto, host, callback), y es asíncrona: cuando la llamada retorna, el socket todavía no está listo.
servidor.listen(3000, '127.0.0.1', () => {
console.error('Escuchando en http://127.0.0.1:3000');
});Ese callback no es error-first: es simplemente un oyente de listening registrado con once. Por eso esta versión hace lo mismo:
servidor.on('listening', () => {
const { address, port } = servidor.address();
console.error(`Escuchando en http://${address}:${port}`);
});
servidor.listen(3000, '127.0.0.1');servidor.address() devuelve { address, family, port } y solo tiene valor después de listening; antes devuelve null. Es la forma correcta de averiguar el puerto real cuando pides el puerto 0, que significa "el sistema operativo elige uno libre" —un truco imprescindible en las pruebas automáticas del Módulo 9, donde no puedes fijar un puerto sin arriesgarte a colisiones.
El segundo argumento, el host, decide quién puede conectarse: '127.0.0.1' acepta solo conexiones desde esta misma máquina, y '0.0.0.0' (el valor por defecto) las acepta por todas las interfaces de red, que es lo que hace falta en contenedores o para probar desde el móvil.
En desarrollo, 127.0.0.1 es la opción prudente: evita que tu servidor a medio hacer quede expuesto a la red de la cafetería. En Docker, en cambio, es un error clásico, porque el contenedor no recibiría el tráfico redirigido desde fuera (lo veremos en el Módulo 11).
- El primer servidor de Escena Viva
Vamos con el código real. Crea src/servidor/servidor.js. La regla que nos autoimponemos desde el minuto uno es la que llevamos defendiendo todo el curso: nada de E/S síncrona dentro del manejador. El catálogo se lee con obtenerCatalogo(), que es asíncrono desde la lección 03-01.
// src/servidor/servidor.js
// Primer servidor HTTP de Escena Viva: responde un resumen del catalogo.
const http = require('node:http');
const { obtenerCatalogo } = require('../catalogo-datos.js');
const PUERTO = Number(process.env.PUERTO) || 3000;
const HOST = process.env.HOST || '127.0.0.1';
// El manejador es ASINCRONO: dentro hay una lectura de disco.
async function manejarPeticion(req, res) {
const eventos = await obtenerCatalogo();
const resumen = {
eventos: eventos.length,
sesiones: eventos.reduce((total, evento) => total + evento.numeroSesiones, 0),
aforoTotal: eventos.reduce((total, evento) => total + evento.aforoTotal, 0),
entradasVendidas: eventos.reduce((total, evento) => total + evento.entradasVendidas, 0)
};
resumen.entradasLibres = resumen.aforoTotal - resumen.entradasVendidas;
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify(resumen, null, 2));
}
function crearServidor() {
// El manejador es async: devuelve una promesa que NADIE recoge. Si falla,
// seria un unhandledRejection (leccion 02-04), asi que se captura aqui.
const servidor = http.createServer((req, res) => {
manejarPeticion(req, res).catch((error) => {
console.error('[error] fallo no controlado:', error);
if (!res.headersSent) {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
}
res.end(JSON.stringify({ error: 'Error interno del servidor' }, null, 2));
});
});
return servidor;
}
function principal() {
const servidor = crearServidor();
servidor.listen(PUERTO, HOST, () => {
const { address, port } = servidor.address();
console.error(`[servidor] escuchando en http://${address}:${port}`);
});
}
if (require.main === module) principal();
module.exports = { crearServidor, manejarPeticion, PUERTO, HOST };Cuatro decisiones que se arrastran durante todo el módulo:
crearServidor()se exporta sin arrancarlo. Separar la construcción del arranque es lo que permitirá levantar el servidor en un puerto aleatorio dentro de una prueba (Módulo 9) sin que el fichero abra un puerto por el mero hecho de ser importado. Elrequire.main === modulees lo que hace que siga siendo ejecutable connode src/servidor/servidor.js.- El
.catchno es opcional.http.createServerignora por completo el valor que devuelva el manejador. Si el manejador esasyncy lanza, la promesa queda rechazada sin dueño: el cliente se queda colgado hasta que expire su tiempo límite y, desde Node 15, el proceso se cae porunhandledRejection. Un solo evento corrupto en el JSON tumbaría el servidor entero. res.headersSentevita el errorERR_HTTP_HEADERS_SENT: si el fallo ocurrió después de empezar a escribir la respuesta, ya no se pueden cambiar las cabeceras. Es el error más común del módulo y lo desmenuzamos en 04-02.- El puerto sale del entorno.
process.env.PUERTOpermite cambiarlo sin tocar el código; en producción lo impone la plataforma. La configuración por entorno es el tema de la lección 11-01.
- Tres formas de probarlo: navegador,
curl y node -e
curl y node -eArranca el servidor:
El navegador es la prueba más rápida (http://localhost:3000), pero también la que menos te enseña: no ves cabeceras ni código de estado, y añade peticiones fantasma como /favicon.ico que ensucian tus registros. Es útil, pero no basta.
curl -i http://localhost:3000/ muestra la respuesta completa, cabeceras incluidas. Es la herramienta que usaremos en todo el módulo:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Transfer-Encoding: chunked
{
"eventos": 3,
"sesiones": 7,
"aforoTotal": 3000,
"entradasVendidas": 1811,
"entradasLibres": 1189
}Ahí están los números de la semilla: 3 eventos, 7 sesiones, aforo 3000, 1811 vendidas y 1189 libres. Nota que no hay Content-Length sino Transfer-Encoding: chunked; en 04-02 explicaremos exactamente por qué y cuándo conviene cada uno.
Banderas de curl que usaremos: -i incluye las cabeceras, -s silencia la barra de progreso, -X fuerza el método, -H añade una cabecera y -d envía un cuerpo.
Y node -e, para probar desde el propio Node con fetch (la API cliente que estudiaremos a fondo en 04-06):
node -e "fetch('http://localhost:3000/').then(r => r.json()).then(d => console.log(d.entradasLibres))"
# 1189
- Por qué el proceso ya no termina solo
Ejecuta el servidor y fíjate en algo que no había pasado nunca en este curso: el proceso no termina. Todos los scripts anteriores hacían su trabajo y devolvían el control a la terminal. Este se queda ahí, aparentemente sin hacer nada.
No es magia, es la cuenta de referencias del bucle de eventos de la lección 02-01. Node sale cuando no quedan operaciones pendientes que puedan producir trabajo futuro. Un socket a la escucha es exactamente eso: un manejador activo que puede despertar el bucle en cualquier momento. Mientras exista, la cuenta no llega a cero y el proceso sigue vivo.
Puedes comprobarlo desactivando esa referencia:
const servidor = http.createServer((req, res) => res.end('hola'));
servidor.listen(3000, () => console.error('escuchando'));
servidor.unref(); // "Si SOLO queda esto, no me mantengas vivo"Ese programa imprime escuchando y termina inmediatamente, sin haber servido nada. El servidor estaba correctamente arrancado; simplemente había dejado de contar. unref() tiene usos legítimos (un servidor de métricas secundario que no debe impedir el cierre), pero para el servidor principal es un fallo garantizado. ref() deshace el efecto.
La consecuencia práctica es que, a partir de ahora, tu proceso tiene ciclo de vida: arranca, sirve durante días y tiene que poder apagarse bien. De eso trata el apartado 8.
- Errores de arranque:
EADDRINUSE y EACCES
EADDRINUSE y EACCESArranca el servidor en una terminal y, sin pararlo, arráncalo en otra:
node:events:496
throw er; // Unhandled 'error' event
Error: listen EADDRINUSE: address already in use 127.0.0.1:3000Reconoce ese mensaje: Unhandled 'error' event es lo de la lección 02-05. Un EventEmitter que emite error sin oyentes lanza la excepción y tumba el proceso. http.Server no es una excepción a la regla.
Los dos fallos de arranque que verás en la práctica:
| Código | Causa | Solución |
|---|---|---|
EADDRINUSE |
El puerto ya está ocupado por otro proceso (a menudo, tu servidor anterior) | Cerrar el otro proceso, o usar otro puerto |
EACCES |
Puertos por debajo de 1024 requieren privilegios de administrador en Linux y macOS | Usar un puerto alto (3000, 8080) y poner un proxy inverso delante en producción |
EACCES sorprende a quien intenta escuchar en el puerto 80 "porque es el de HTTP". En sistemas tipo Unix, los puertos menores de 1024 son privilegios reservados: solo root puede abrirlos. La respuesta correcta nunca es ejecutar Node como root, sino escuchar en un puerto alto y dejar que Nginx o el balanceador de la plataforma escuchen en el 80/443 (Módulo 11).
El arreglo es registrar un oyente de error antes de llamar a listen, y dar un mensaje que se entienda:
servidor.on('error', (error) => {
if (error.code === 'EADDRINUSE') {
console.error(`[servidor] el puerto ${PUERTO} ya esta en uso. Prueba: PUERTO=3001 node ${__filename}`);
} else if (error.code === 'EACCES') {
console.error(`[servidor] sin permisos para el puerto ${PUERTO}. Usa un puerto >= 1024.`);
} else {
console.error('[servidor] error inesperado:', error);
}
process.exitCode = 1;
});error.code (en inglés, propiedad del sistema) es lo que llega de libuv; no lo confundas con nuestro error.codigo de dominio, que traduciremos a estados HTTP en la próxima lección. Y process.exitCode = 1 en vez de process.exit(1): deja que el proceso termine de forma natural vaciando sus buffers de salida, como ya vimos en el Módulo 1.
Para localizar al culpable de un EADDRINUSE en Linux o macOS: lsof -i :3000 te dice qué proceso tiene el puerto, y kill <PID> lo libera.
- Apagado ordenado:
server.close() y SIGINT
server.close() y SIGINTCuando pulsas Ctrl+C, tu terminal envía la señal SIGINT al proceso, y el comportamiento por defecto de Node es morir en el acto. Eso es aceptable en un script; en un servidor, significa cortar a media frase las peticiones en curso: un cliente se queda sin su respuesta, y una escritura de fichero puede quedar a medias.
El apagado ordenado consiste en dejar de aceptar conexiones nuevas y esperar a que terminen las vivas:
function instalarApagadoOrdenado(servidor) {
let apagando = false;
function apagar(senal) {
if (apagando) return; // Un segundo Ctrl+C no debe reentrar aqui
apagando = true;
console.error(`\n[servidor] recibida ${senal}, cerrando...`);
// 1. Deja de aceptar conexiones NUEVAS. El callback se llama
// cuando la ultima conexion viva se ha cerrado.
servidor.close((error) => {
if (error) process.exitCode = 1;
console.error(error ? `[servidor] error al cerrar: ${error.message}` : '[servidor] cerrado limpiamente');
});
// 2. Cierra las conexiones ociosas keep-alive, que si no
// mantendrian el proceso vivo hasta su tiempo limite.
servidor.closeIdleConnections();
// 3. Red de seguridad: si en 10 s no ha cerrado, forzar.
const plazo = setTimeout(() => {
console.error('[servidor] plazo agotado, cierre forzado');
servidor.closeAllConnections();
process.exit(1);
}, 10_000);
plazo.unref(); // Este temporizador no debe mantener vivo el proceso
}
process.on('SIGINT', () => apagar('SIGINT'));
process.on('SIGTERM', () => apagar('SIGTERM'));
}El punto delicado es el 2, y viene de lo que vimos en el apartado 2: close() no cierra las conexiones existentes, solo el socket a la escucha. Con keep-alive, un navegador que te ha pedido una página deja su conexión TCP abierta unos segundos por si pide algo más. Esas conexiones ociosas no están sirviendo nada, pero cuentan como vivas, así que close() se queda esperando y parece que tu servidor "no cierra". closeIdleConnections() (Node 18.2+) despacha exactamente ese caso: cierra las que no tienen una petición en curso y respeta las que sí.
Sobre las señales: SIGINT es tu Ctrl+C; SIGTERM es la que envían Docker, systemd y las plataformas de despliegue para pedir un cierre ordenado, con un plazo tras el cual mandan SIGKILL, que no se puede capturar. Por eso el temporizador de seguridad: más vale cerrar tú en 10 segundos que dejar que te maten a los 30 dejando ficheros a medias. En el Módulo 11 volveremos sobre esto con PM2 y Docker.
Errores Comunes y Consejos
- Olvidar
res.end(). El cliente se queda esperando indefinidamente. En el navegador se ve como una pestaña que gira sin fin, y encurlcomo una respuesta que nunca llega. Toda ruta de ejecución del manejador debe terminar en unend(). - Hacer E/S síncrona en el manejador. Un
readFileSyncen una ruta bloquea el bucle de eventos y, por tanto, a todos los clientes a la vez, no solo al que la pidió. Es aceptable en el arranque; jamás dentro derequest. - Manejador
asyncsin.catch. Promesa rechazada sin dueño, cliente colgado y proceso caído. Envuelve siempre, como hacecrearServidor(). - No escuchar el evento
errordel servidor. UnEADDRINUSEte tumba el proceso con una traza incomprensible en vez de un mensaje útil. - Confundir
connectionconrequest. Conkeep-alive, una conexión sirve muchas peticiones. Contar conexiones no es contar visitas. - Consejo: exporta
crearServidor()sin arrancarlo y arranca solo bajorequire.main === module. Tu yo del Módulo 9, escribiendo pruebas de integración, te lo agradecerá. - Consejo: durante el desarrollo,
node --watch src/servidor/servidor.jsreinicia el proceso al guardar. Sin dependencias y sinnodemon.
Ejercicios
Ejercicio 1: contador de conexiones y peticiones
Amplía src/servidor/servidor.js para que lleve la cuenta de conexiones TCP y de peticiones HTTP servidas, y las exponga en el resumen JSON bajo las claves conexiones y peticiones. Después, con el servidor arrancado, ejecuta curl tres veces seguidas y luego recarga la página del navegador tres veces. Explica por qué los dos números no crecen igual.
Ejercicio 2: arranque robusto
Escribe src/servidor/arrancar.js que reciba el puerto por process.argv (con process.env.PUERTO como segunda opción y 3000 como tercera), arranque el servidor y trate los tres casos: arranque correcto (imprime la URL real obtenida de servidor.address()), EADDRINUSE (mensaje claro y exitCode 1) y EACCES. Pruébalo con los puertos 3000, 80 y con dos instancias a la vez.
Ejercicio 3: medir el apagado ordenado
Añade una ruta lenta al manejador: si req.url es /lento, espera 5 segundos con dormir (de src/utiles/dormir.js) antes de responder. Lanza curl http://localhost:3000/lento y, mientras espera, pulsa Ctrl+C en el servidor. Comprueba que la petición se completa y que solo después aparece cerrado limpiamente. Repite quitando closeIdleConnections() y explica qué cambia.
Soluciones
Solución 1. Los contadores son estado del servidor, así que viven en crearServidor:
function crearServidor() {
const estadisticas = { conexiones: 0, peticiones: 0 };
const servidor = http.createServer((req, res) => {
estadisticas.peticiones++;
manejarPeticion(req, res, estadisticas).catch(/* ... */);
});
servidor.on('connection', () => { estadisticas.conexiones++; });
return servidor;
}Tres curl producen 3 conexiones y 3 peticiones: curl cierra su conexión al terminar cada invocación. Tres recargas del navegador producen normalmente 1 conexión y 6 peticiones o más: el navegador reutiliza la conexión gracias a keep-alive y, además, pide /favicon.ico por su cuenta. Esa asimetría es justo la razón de existir de closeIdleConnections().
Solución 2. La clave es que el oyente de error se registre antes de listen, porque el fallo se emite de forma asíncrona pero inmediata:
const puerto = Number(process.argv[2]) || Number(process.env.PUERTO) || 3000;
const servidor = crearServidor();
servidor.on('error', (error) => {
const mensajes = {
EADDRINUSE: `El puerto ${puerto} esta ocupado. Prueba: node src/servidor/arrancar.js 3001`,
EACCES: `Sin permisos para el puerto ${puerto}. Usa uno >= 1024.`
};
console.error(`[servidor] ${mensajes[error.code] ?? error.message}`);
process.exitCode = 1;
});
servidor.listen(puerto, '127.0.0.1', () => {
const { address, port } = servidor.address();
console.error(`[servidor] escuchando en http://${address}:${port}`);
});Con el puerto 80 sin privilegios obtienes EACCES; con dos instancias en el 3000, la segunda da EADDRINUSE. Sin el oyente, ambos casos serían un volcado de pila con Unhandled 'error' event.
Solución 3. Con closeIdleConnections() verás que la petición lenta se completa y que el proceso termina un instante después. Sin ella, si antes has usado el navegador, el proceso se queda colgado hasta que expira el keep-alive (5 segundos por defecto, server.keepAliveTimeout), o incluso hasta el plazo forzado de 10 segundos si el navegador renueva la conexión. Es el síntoma exacto de "mi servidor no cierra con Ctrl+C", y ahora sabes que la culpa no es de tu código, sino de una conexión ociosa que sigue contando.
Conclusión
Escena Viva ya está en la red. Y lo ha conseguido sin conceptos nuevos: http.createServer devuelve un EventEmitter que emite request —da igual si pasas el manejador al constructor o lo registras con on—, además de connection, listening, close y ese error que, si no escuchas, te tumba el proceso igual que en la lección 02-05. listen(puerto, host, callback) es asíncrono y su callback no es más que un once('listening'); server.address() te da el puerto real, imprescindible cuando pides el puerto 0.
Tienes el primer servidor real en src/servidor/servidor.js, con la disciplina que mantendremos todo el módulo: manejador asíncrono desde el minuto uno, .catch obligatorio para que una promesa rechazada no te cueste el proceso, puerto configurable con process.env.PUERTO y crearServidor() exportado sin arrancar. Sabes probarlo con el navegador, con curl -i —que enseña las cabeceras— y con node -e. Entiendes por qué el proceso ya no termina solo: el socket a la escucha mantiene la cuenta de referencias del bucle de eventos, y unref() lo demuestra apagándolo. Y sabes arrancarlo y apagarlo bien: EADDRINUSE y EACCES capturados con mensajes útiles, y un cierre ordenado con SIGINT/SIGTERM, server.close() y closeIdleConnections(), porque las conexiones persistentes no se cierran solas.
Lo que hace nuestro servidor, sin embargo, es bochornoso: responde lo mismo a /, a /eventos y a /cualquier-cosa, siempre con un 200, sin mirar el método ni los parámetros. Es hora de leer de verdad lo que nos llega y de construir con cuidado lo que devolvemos. En la próxima lección, Manejo de Solicitudes y Respuestas, desmontamos req —que es un stream de lectura— y res —que es un stream de escritura—, aprendemos a parsear la URL sin cortar cadenas a mano, repasamos los códigos de estado que usará el curso entero y construimos src/servidor/respuestas.js con la tabla que traduce nuestros error.codigo de dominio a estados HTTP.
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
