Módulos HTTP/HTTPS
El ecosistema de Node.js incluye una serie de módulos internos que permiten construir servidores web sin necesidad de instalar ninguna librería externa. Entre ellos, los más importantes para el desarrollo backend básico son los módulos http y https. Comprender cómo funcionan es fundamental, porque representan el mecanismo de más bajo nivel para escuchar peticiones y responderlas.
Qué son los módulos http y https
Node no incluye un servidor web listo para usar. En su lugar, ofrece las herramientas necesarias para que seas tú quien lo construya. El módulo http proporciona la capacidad de recibir una petición desde un cliente y enviar una respuesta. El módulo https funciona de forma idéntica, pero añade la capa de cifrado TLS para garantizar una comunicación segura.
Son módulos escritos en C++ y JavaScript que vienen integrados en Node. No se instalan y no tienen dependencias externas.
Importar los módulos en ES Modules
Cuando se trabaja con ES Modules, la importación se hace de la siguiente manera:
import http from "node:http";
import https from "node:https";
Las rutas que comienzan con node: indican que provienen de la librería estándar.
Crear un servidor básico con http
El servidor más simple posible consta de dos partes: una función que se ejecuta en cada petición y una orden para escuchar un puerto.
// Importar el módulo HTTP nativo de Node.js
// El prefijo "node:" indica que es un módulo interno de Node.js (opcional pero recomendado)
import http from "node:http";
// Crear un servidor HTTP usando el método createServer
// Este método recibe una función callback que se ejecuta en CADA solicitud entrante
const server = http.createServer((req, res) => {
// req (request/solicitud): Objeto que contiene toda la información de la solicitud del cliente
// - req.url: La URL solicitada (ej: "/", "/api", "/usuarios")
// - req.method: El método HTTP usado (GET, POST, PUT, DELETE, etc.)
// - req.headers: Los encabezados de la solicitud
// res (response/respuesta): Objeto que usamos para construir y enviar la respuesta al cliente
// Establecer el código de estado HTTP de la respuesta
// 200 = OK (éxito) - indica que la solicitud se procesó correctamente
res.statusCode = 200;
// Establecer el encabezado Content-Type para indicar el tipo de contenido de la respuesta
// "text/plain" significa que estamos enviando texto plano sin formato
// Otros tipos comunes: "text/html", "application/json", "application/xml"
res.setHeader("Content-Type", "text/plain");
// Enviar el cuerpo de la respuesta y finalizar la comunicación
// res.end() hace dos cosas:
// 1. Escribe el contenido en el cuerpo de la respuesta
// 2. Cierra la conexión (indica que la respuesta está completa)
res.end("Servidor Node nativo funcionando");
// IMPORTANTE: Después de llamar a res.end(), NO se puede enviar más datos
});
// Configurar el servidor para que escuche conexiones entrantes en el puerto 3000
// El método listen() inicia el servidor y lo deja esperando solicitudes
server.listen(3000, () => {
// Esta función callback es OPCIONAL y se ejecuta UNA SOLA VEZ cuando el servidor
// está listo y comenzando a aceptar conexiones
// Mensaje útil para saber que el servidor se inició correctamente
console.log("Servidor escuchando en http://localhost:3000");
// Ahora puedes abrir tu navegador y visitar:
// http://localhost:3000
// http://localhost:3000/cualquier-ruta
// ¡Todas las rutas mostrarán el mismo mensaje!
});
Explicación del funcionamiento
Cada vez que un cliente accede a localhost:3000, Node ejecuta la función pasada a createServer. Esta función recibe dos objetos clave:
- req: contiene información de la petición, como el método (GET, POST), la URL, las cabeceras y, en caso de POST, el cuerpo.
- res: representa la respuesta que enviaremos al cliente y permite configurar estado, cabeceras y contenido.
Node no interpreta rutas, no procesa JSON y no sabe servir archivos. Todo eso debe implementarse manualmente.
Comprender req y res
El objeto req contiene datos sobre la petición:
req.method: GET, POST, PUT, DELETE.req.url: ruta solicitada.req.headers: cabeceras enviadas por el cliente.req.on("data"): permite leer el cuerpo en peticiones POST.
El objeto res permite construir una respuesta:
res.statusCode: código HTTP.res.setHeader: cabeceras personalizadas.res.end: finaliza la respuesta y envía datos.
Detectar rutas manualmente con Node
Como Node no ofrece un sistema de enrutado, el programador debe comprobar manualmente la combinación de método y URL.
// Importar el módulo HTTP nativo de Node.js
// El "node:" es un esquema de prefijo para módulos nativos de Node.js
import http from "node:http";
// Crear un servidor HTTP usando el método createServer
// Este método recibe una función callback que se ejecuta en cada solicitud
const server = http.createServer((req, res) => {
// req (request): objeto que contiene información sobre la solicitud del cliente
// res (response): objeto que usamos para enviar una respuesta al cliente
// Verificar si la solicitud es para la ruta principal ("/") y usando el método GET
if (req.url === "/" && req.method === "GET") {
// Establecer el encabezado Content-Type para indicar que la respuesta es texto plano
res.setHeader("Content-Type", "text/plain");
// Enviar la respuesta con el texto "Página principal" y finalizar la respuesta
res.end("Página principal");
}
// Verificar si la solicitud es para la ruta "/api" usando el método GET
else if (req.url === "/api" && req.method === "GET") {
// Establecer el encabezado Content-Type para indicar que la respuesta es JSON
res.setHeader("Content-Type", "application/json");
// Enviar una respuesta JSON
// JSON.stringify convierte un objeto JavaScript a una cadena JSON
res.end(JSON.stringify({ mensaje: "API funcionando" }));
}
// Manejar cualquier otra ruta que no coincida con las anteriores
else {
// Establecer el código de estado HTTP a 404 (No encontrado)
res.statusCode = 404;
// Enviar respuesta indicando que el recurso no fue encontrado
res.end("No encontrado");
}
});
// Hacer que el servidor escuche en el puerto 3000
// Una vez ejecutado, el servidor estará disponible en: http://localhost:3000
server.listen(3000, () => {
// Esta función callback es opcional y se ejecuta cuando el servidor comienza a escuchar
console.log("Servidor ejecutándose en http://localhost:3000");
});
Este estilo permite comprender perfectamente cómo trabajan los frameworks internamente. Express, por ejemplo, no es más que una capa que automatiza este tipo de comprobaciones.
Crear un servidor https
El módulo https funciona igual que http, pero necesita certificados para cifrar la conexión. Incluso en desarrollo se puede usar un certificado autofirmado.
// Importar el módulo HTTPS nativo de Node.js
// HTTPS es HTTP seguro, usa cifrado SSL/TLS para proteger la comunicación
import https from "node:https";
// Importar la función readFileSync del módulo fs (file system)
// readFileSync lee archivos de manera síncrona (bloqueante)
import { readFileSync } from "node:fs";
// Configurar las opciones para el servidor HTTPS
// Estas opciones son OBLIGATORIAS para crear un servidor HTTPS
const options = {
// key: Clave privada del servidor (debe mantenerse segura y privada)
// Se usa para cifrar la comunicación y demostrar la identidad del servidor
key: readFileSync("./clave-privada.key"),
// cert: Certificado público del servidor
// Contiene la clave pública y información de identificación verificado por una CA
cert: readFileSync("./certificado.crt"),
// OTRAS OPCIONES POSIBLES (no usadas en este ejemplo):
// ca: Certificados de autoridad certificadora intermedia
// passphrase: Contraseña para la clave privada (si está protegida)
// requestCert: true, // Solicitar certificado al cliente (autenticación mutua)
// rejectUnauthorized: true // Rechazar conexiones no autorizadas
};
// Crear un servidor HTTPS
// A diferencia de HTTP, HTTPS requiere las opciones de certificado
const server = https.createServer(
options, // Primer parámetro: opciones de SSL/TLS
(req, res) => {
// Segundo parámetro: manejador de solicitudes (igual que HTTP)
// req: Solicitud - contiene información de la petición del cliente (cifrada)
// res: Respuesta - para enviar datos al cliente (también cifrados)
// Enviar respuesta simple y finalizar la conexión
res.end("Servidor HTTPS en Node");
// TODA la comunicación aquí es automáticamente cifrada gracias a HTTPS
// incluyendo headers, cuerpo, cookies, etc.
}
);
// Configurar el servidor para escuchar en el puerto 3001
// Nota: HTTPS tradicionalmente usa el puerto 443, pero para desarrollo usamos 3001
server.listen(3001, () => {
// Callback que se ejecuta cuando el servidor está listo
console.log("HTTPS en https://localhost:3001");
// IMPORTANTE: La URL usa https:// no http://
// Los navegadores mostrarán un candado cuando la conexión sea segura
});
La lógica interna es la misma; simplemente se exige la configuración criptográfica para establecer una conexión segura.
¿Qué es HTTPS?
- HTTP + SSL/TLS: Protocolo HTTP con una capa de seguridad adicional
- Cifrado: Los datos se transmiten cifrados, imposibles de leer por intermediarios
- Autenticación: Verifica que te estás conectando al servidor legítimo
- Integridad: Garantiza que los datos no fueron modificados en tránsito
Archivos de certificados necesarios:
1. clave-privada.key (PRIVADA)
# Ejemplo de cómo se ve (NUNCA compartir):
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCw...
-----END PRIVATE KEY-----
2. certificado.crt (PÚBLICA)
# Ejemplo de cómo se ve (se puede compartir):
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKL7wQ8O3uGFMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV...
-----END CERTIFICATE-----
Cómo generar certificados para desarrollo:
Opción 1: Certificado autofirmado (para testing)
# Generar clave privada y certificado autofirmado
openssl req -x509 -newkey rsa:4096 -nodes -keyout clave-privada.key -out certificado.crt -days 365
# Esto creará:
# - clave-privada.key (tu clave privada)
# - certificado.crt (certificado autofirmado válido por 365 días)
Opción 2: Usar mkcert (recomendado para desarrollo)
# Instalar mkcert
brew install mkcert # macOS
# o
sudo apt install mkcert # Linux
# Configurar autoridad local
mkcert -install
# Generar certificado para localhost
mkcert localhost 127.0.0.1 ::1
# Esto creará:
# - localhost+2-key.pem (clave privada)
# - localhost+2.pem (certificado)
Diferencias clave vs servidor HTTP normal:
| Aspecto | HTTP | HTTPS |
|---|---|---|
| Puerto | 3000 | 3001 (producción: 443) |
| URL | http:// | https:// |
| Seguridad | Texto plano | Cifrado |
| Rendimiento | Más rápido | Ligero overhead por cifrado |
| Certificados | No requiere | Requiere clave y certificado |
Comportamiento en el navegador:
- 🔒 Certificado válido: Candado verde, conexión segura
- ⚠️ Autofirmado: Advertencia "No seguro" (puedes continuar en desarrollo)
- ❌ Sin certificado: No funcionará - conexión rechazada
Para probar el servidor:
# Ejecutar el servidor (asegúrate de tener los archivos .key y .crt)
node servidor-https.js
# Probar con curl (ignorar verificación de certificado en desarrollo)
curl -k <https://localhost:3001>
# Probar en navegador: <https://localhost:3001>
Este servidor es esencial para aplicaciones web modernas que manejan datos sensibles como contraseñas, información personal, o pagos.
Idea clave
El servidor de Node no es más que una función que se ejecuta cada vez que llega una petición. Toda la lógica adicional (rutas, parseo del body, controladores, validaciones, plantillas, APIs REST) se construye encima.
Cómo manejar el body de un POST en Node nativo
Node no procesa el cuerpo de una petición POST de manera automática. Hay que leer los datos manualmente escuchando los eventos del objeto req.
// Importar el módulo HTTP nativo de Node.js
import http from "node:http";
// Crear un servidor HTTP
const server = http.createServer((req, res) => {
// Verificar si la solicitud es para la ruta "/datos" y usa el método POST
if (req.url === "/datos" && req.method === "POST") {
// Variable para acumular los datos del cuerpo de la solicitud
let cuerpo = "";
// MANEJO DE STREAM DE DATOS:
// En Node.js, las solicitudes HTTP son Readable Streams (flujos de lectura)
// Esto significa que los datos pueden llegar en múltiples fragmentos (chunks)
// especialmente si son grandes
// Evento 'data': Se dispara CADA VEZ que llega un fragmento de datos
req.on("data", (chunk) => {
// 'chunk' es un Buffer (datos binarios) o String con un fragmento de los datos
// Concatenamos cada fragmento a nuestra variable 'cuerpo'
cuerpo += chunk;
// EJEMPLO PRÁCTICO:
// Si el cliente envía '{"nombre":"Juan","edad":30}'
// Podría llegar en 2 chunks: '{"nombre":"Juan"' y ',"edad":30}'
// Por eso necesitamos acumularlos
});
// Evento 'end': Se dispara cuando TODOS los datos han sido recibidos
req.on("end", () => {
// En este punto, 'cuerpo' contiene todos los datos completos
try {
// Intentar parsear el cuerpo como JSON
// JSON.parse() convierte una cadena JSON en un objeto JavaScript
const datos = JSON.parse(cuerpo);
// Configurar la respuesta como JSON
res.setHeader("Content-Type", "application/json");
// Enviar respuesta de confirmación con los datos recibidos
res.end(
JSON.stringify({
recibido: true,
datos: datos, // Devolvemos los mismos datos que recibimos
})
);
} catch (error) {
// Si el JSON es inválido, capturamos el error
res.statusCode = 400; // Bad Request
res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify({
error: "JSON inválido",
mensaje: "El cuerpo de la solicitud no contiene JSON válido",
})
);
}
});
// Manejo de errores en el stream de la solicitud
req.on("error", (error) => {
res.statusCode = 500; // Internal Server Error
res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify({
error: "Error al recibir datos",
mensaje: error.message,
})
);
});
} else {
// Si la ruta o método no coinciden, devolver 404
res.statusCode = 404;
res.end("Ruta no encontrada");
}
});
// Iniciar el servidor en el puerto 3000
server.listen(3000, () => {
console.log("Servidor escuchando en <http://localhost:3000>");
console.log("Endpoint disponible: POST <http://localhost:3000/datos>");
});
Explicación detallada del flujo de datos POST:
¿Cómo funcionan los streams en Node.js?
CLIENTE ENVÍA: {"nombre":"Ana","edad":25}
↓
SERVIDOR RECIBE:
chunk1: '{"nombre":"Ana"'
chunk2: ',"edad":25}'
↓
ACUMULACIÓN: cuerpo = '{"nombre":"Ana"' + ',"edad":25}'
↓
PARSEO: JSON.parse('{"nombre":"Ana","edad":25}')
↓
OBJETO: { nombre: "Ana", edad: 25 }
Eventos clave del Readable Stream (req):
| Evento | Se dispara cuando... | Uso típico |
|---|---|---|
'data' | Llega un fragmento de datos | Acumular datos |
'end' | Terminó de llegar todos los datos | Procesar datos completos |
'error' | Hay un error en la recepción | Manejar errores |
Mejoras de seguridad y robustez añadidas:
// VERSIÓN MEJORADA CON MÁS CONTROLES:
req.on("data", (chunk) => {
cuerpo += chunk;
// Límite de tamaño para prevenir ataques de DoS
if (cuerpo.length > 1e6) {
// 1MB límite
req.destroy(); // Terminar la conexión
res.statusCode = 413; // Payload Too Large
res.end("Demasiados datos");
}
});
req.on("end", () => {
// Verificar que hay datos antes de parsear
if (!cuerpo.trim()) {
res.statusCode = 400;
return res.end(JSON.stringify({ error: "Cuerpo vacío" }));
}
try {
const datos = JSON.parse(cuerpo);
// Procesar datos...
} catch (error) {
// Manejo específico de errores de JSON
res.statusCode = 400;
res.end(JSON.stringify({ error: "JSON malformado" }));
}
});
Cómo probar este servidor:
1. Usando curl:
curl -X POST <http://localhost:3000/datos> \\
-H "Content-Type: application/json" \\
-d '{"nombre":"Maria","edad":30}'
2. Usando JavaScript en el navegador:
fetch("<http://localhost:3000/datos>", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
producto: "Laptop",
precio: 999.99,
stock: 15,
}),
})
.then((response) => response.json())
.then((data) => console.log(data));
3. Usando herramientas como Postman o Thunder Client:
- Método: POST
- URL:
http://localhost:3000/datos - Headers:
Content-Type: application/json - Body (raw JSON):
{
"usuario": "pedro",
"email": "pedro@ejemplo.com",
"activo": true
}
Casos de error que maneja:
- JSON inválido:
{"nombre": "Juan(falta llave de cierre) - Cuerpo vacío: Solicitud POST sin datos
- Ruta incorrecta: GET a
/datoso POST a otra ruta - Datos muy grandes: Prevención de ataques DoS
Este código es fundamental para entender cómo Node.js maneja datos en tiempo real y es la base para construir APIs REST que reciben información de formularios, aplicaciones móviles, o otros servicios.
Cómo servir archivos estáticos sin frameworks
Node tampoco tiene una función nativa para servir HTML, CSS o imágenes. Hay que leer manualmente el archivo y enviarlo.
// Importar el módulo HTTP nativo de Node.js
import http from "node:http";
// Importar la función readFile del módulo fs/promises
// fs/promises proporciona versiones basadas en Promesas de las funciones del filesystem
// readFile lee archivos de manera asíncrona (no bloqueante) usando async/await
import { readFile } from "node:fs/promises";
// Crear un servidor HTTP con una función ASYNC
// La palabra clave 'async' permite usar 'await' dentro de la función
const server = http.createServer(async (req, res) => {
// req: objeto de solicitud - contiene información de la petición del cliente
// res: objeto de respuesta - para enviar datos al cliente
// Verificar si la solicitud es para la ruta raíz ("/") y método GET
if (req.url === "/" && req.method === "GET") {
try {
// LEER ARCHIVO HTML DE MANERA ASÍNCRONA:
// await pausa la ejecución hasta que la promesa se resuelve
// readFile retorna una Promesa que se resuelve con el contenido del archivo
// Parámetros de readFile:
// 1. "./public/index.html" - ruta del archivo a leer
// 2. "utf-8" - codificación (convierte el Buffer a string legible)
const contenido = await readFile("./public/index.html", "utf-8");
// Establecer el encabezado Content-Type como HTML
// Esto le indica al navegador que interprete el contenido como HTML
res.setHeader("Content-Type", "text/html");
// Enviar el contenido del archivo HTML como respuesta
res.end(contenido);
} catch (error) {
// Manejar errores si el archivo no existe o no se puede leer
console.error("Error leyendo archivo:", error);
res.statusCode = 500; // Internal Server Error
res.setHeader("Content-Type", "text/html");
res.end(`
<html>
<body>
<h1>Error 500 - Error del servidor</h1>
<p>No se pudo cargar la página solicitada</p>
</body>
</html>
`);
}
} else {
// Si la ruta no es "/" o el método no es GET, devolver 404
res.statusCode = 404; // Not Found
res.setHeader("Content-Type", "text/html");
res.end(`
<html>
<body>
<h1>Error 404 - Página no encontrada</h1>
<p>La ruta ${req.url} no existe en este servidor</p>
</body>
</html>
`);
}
});
// Iniciar el servidor en el puerto 3000
server.listen(3000, () => {
console.log("Servidor escuchando en <http://localhost:3000>");
console.log("Sirviendo archivos estáticos desde ./public/");
});
Explicación detallada del funcionamiento:
Estructura de archivos necesaria:
proyecto/
├── server.js (este archivo)
└── public/
└── index.html (archivo que se servirá)
Contenido esperado de public/index.html:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mi Servidor Node.js</title>
</head>
<body>
<h1>¡Bienvenido a mi servidor!</h1>
<p>Este archivo HTML está siendo servido por Node.js nativo</p>
</body>
</html>
Flujo de una solicitud:
CLIENTE → Solicita: GET <http://localhost:3000/>
↓
SERVIDOR → Detecta ruta "/" y método "GET"
↓
SERVIDOR → Lee archivo: ./public/index.html
↓
SERVIDOR → Envía contenido HTML + headers
↓
CLIENTE → Navegador renderiza el HTML
Diferencias clave con versiones anteriores:
readFile vs readFileSync:
// SÍNCRONO (bloqueante) - versión anterior
const contenido = readFileSync("./archivo.html", "utf-8");
// ASÍNCRONO (no bloqueante) - esta versión
const contenido = await readFile("./archivo.html", "utf-8");
Ventajas del enfoque asíncrono:
- No bloqueante: El servidor puede manejar otras solicitudes mientras lee el archivo
- Mejor rendimiento: Múltiples usuarios pueden ser atendidos simultáneamente
- Manejo moderno: Usa Promesas y async/await (estándar ES6+)
Mejoras adicionales recomendadas:
// VERSIÓN MEJORADA CON MÁS TIPOS DE ARCHIVOS
const server = http.createServer(async (req, res) => {
try {
let filePath = "";
let contentType = "text/html";
// Determinar archivo y tipo de contenido según la ruta
if (req.url === "/") {
filePath = "./public/index.html";
} else if (req.url === "/styles.css") {
filePath = "./public/styles.css";
contentType = "text/css";
} else if (req.url === "/script.js") {
filePath = "./public/script.js";
contentType = "application/javascript";
} else {
// Si no coincide ninguna ruta conocida
res.statusCode = 404;
return res.end("Archivo no encontrado");
}
// Leer y servir el archivo
const contenido = await readFile(filePath, "utf-8");
res.setHeader("Content-Type", contentType);
res.end(contenido);
} catch (error) {
console.error("Error:", error);
res.statusCode = 500;
res.end("Error del servidor");
}
});
Cómo probar el servidor:
1. Crear la estructura de archivos:
mkdir public
echo '<!DOCTYPE html><html><head><title>Mi Página</title></head><body><h1>¡Funciona!</h1></body></html>' > public/index.html
2. Ejecutar el servidor:
node server.js
3. Probar en el navegador:
- Abrir:
http://localhost:3000/ - Deberías ver el contenido de
index.html
4. Probar rutas incorrectas:
http://localhost:3000/otra-ruta→ Mostrará error 404
Posibles errores y soluciones:
// ERROR COMÚN: Archivo no encontrado
// SOLUCIÓN: Verificar que la ruta ./public/index.html existe
// ERROR: Permisos insuficientes
// SOLUCIÓN: Verificar permisos de lectura en el directorio public/
// ERROR: Codificación incorrecta
// SOLUCIÓN: Especificar "utf-8" para archivos de texto
Este código representa los fundamentos de un servidor web estático, similar a lo que hacen frameworks como Express.js detrás de escenas, pero implementado manualmente con las capacidades nativas de Node.js.
Explicación detallada de req y res. Comprender a fondo los objetos req y res en Node.js nativo
Cuando se crea un servidor con el módulo http, la función que se ejecuta en cada petición recibe dos parámetros fundamentales: req (request) y res (response). Estos dos objetos representan todo lo que ocurre durante el proceso de comunicación entre el cliente y el servidor.
Comprenderlos de manera detallada es esencial para dominar el backend con Node nativo, ya que son la base de cualquier operación posterior, tanto en aplicaciones simples como en arquitecturas de APIs REST más grandes.
El objeto req: información que llega del cliente
El objeto req es un contenedor que agrupa toda la información enviada desde el navegador, un archivo HTML, una herramienta de pruebas como Thunder Client o cualquier cliente HTTP.
Dentro de este objeto se pueden consultar:
Método HTTP
Representa el tipo de operación solicitada:
req.method; // "GET", "POST", "PUT", "DELETE"...
Node no valida ni interpreta el método. Solo lo entrega.
URL solicitada
Es la ruta exacta que envió el cliente:
req.url; // "/productos", "/api/usuarios?id=3", "/"
Node tampoco divide la ruta en segmentos ni interpreta parámetros. Todo llega como una cadena sin procesar. Si se necesitan parámetros, deben extraerse manualmente.
Cabeceras enviadas por el cliente
El navegador siempre envía cabeceras, como el tipo de contenido, el lenguaje, el agente de usuario o las cookies.
req.headers;
Por ejemplo, si un cliente envía JSON:
req.headers["content-type"]; // "application/json"
El cuerpo (body) de la petición
En las peticiones GET no suele haber cuerpo, pero en POST, PUT o PATCH sí lo hay. Node no lo trae montado automáticamente. Se recibe en forma de stream, es decir, en pequeños fragmentos.
let cuerpo = "";
req.on("data", (chunk) => {
cuerpo += chunk;
});
req.on("end", () => {
// el cuerpo completo ya está disponible
});
Esto enseña al alumno la diferencia entre:
- Peticiones sin datos (GET).
- Peticiones con datos fragmentados (POST).
- Flujo asíncrono de datos en Node.
Otros elementos útiles de req
El objeto también permite acceder a más detalles como:
- Direcciones IP
- Estado de la conexión
- Parámetros del protocolo HTTP
- Si la petición usa HTTPS
No es necesario entender todo esto al principio, pero es importante saber que existe.
El objeto res: cómo responde el servidor
El objeto res permite construir y enviar una respuesta al cliente. Node no crea respuestas automáticas. El programador debe definir manualmente:
- código de estado
- cabeceras
- contenido final
Código de estado
Representa el resultado de la operación:
res.statusCode = 200;
res.statusCode = 404;
res.statusCode = 500;
Si no se indica nada, Node devuelve 200 por defecto.
Cabeceras de la respuesta
Las cabeceras permiten especificar qué tipo de datos se están enviando:
res.setHeader("Content-Type", "text/plain");
res.setHeader("Content-Type", "application/json");
res.setHeader("Content-Type", "text/html");
Sin una cabecera correcta, el navegador puede interpretar mal la respuesta.
Enviar contenido
La respuesta se finaliza y se envían los datos con:
res.end("Hola mundo");
Se debe llamar siempre a res.end, incluso si no se quiere enviar contenido adicional.
Enviar JSON
Node no envía JSON automáticamente. Hay que convertir el objeto manualmente:
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ ok: true, mensaje: "Todo bien" }));
Si el alumno entiende esto, podrá trabajar con APIs sin frameworks.
Enviar HTML
De igual forma, Node permite enviar HTML:
res.setHeader("Content-Type", "text/html");
res.end("<h1>Bienvenido</h1>");
Node no interpreta plantillas ni genera páginas dinámicas sin librerías adicionales.
Control de errores
El servidor puede enviar mensajes personalizados:
res.statusCode = 500;
res.end("Error interno del servidor");
Ciclo completo entre req y res
Para visualizar el flujo completo, es útil mostrarlo con un ejemplo mínimo:
import http from "node:http";
const server = http.createServer((req, res) => {
console.log("Método:", req.method);
console.log("URL:", req.url);
res.setHeader("Content-Type", "text/plain");
res.end("Petición recibida");
});
server.listen(3000);
Esto demuestra la esencia: cada petición ejecuta siempre la misma función, pero con distintos valores según lo que envíe el cliente.
4. Por qué es importante entender req y res antes de usar frameworks
Los frameworks como Express simplifican gran parte del trabajo con funciones como:
req.bodyreq.paramsreq.queryres.json()res.sendFile()res.status()
Estas funciones no existen en Node nativo. Son utilidades añadidas para hacer más cómodo el desarrollo.
Aprender cómo funcionan req y res desde cero ayuda a comprender qué está haciendo Express por debajo y evita errores en proyectos más avanzados.
Mini ejemplo completo integrando todo
Para cerrar la ampliación, un servidor que detecta rutas, lee JSON y responde adecuadamente:
// Importar el módulo HTTP nativo de Node.js
import http from "node:http";
// Crear un servidor HTTP que maneja múltiples rutas y métodos
const server = http.createServer((req, res) => {
// req: objeto de solicitud - contiene URL, método HTTP, headers, etc.
// res: objeto de respuesta - para enviar respuestas al cliente
// ==========================================================================
// RUTA 1: Página de inicio (raíz) - Solo acepta GET
// ==========================================================================
if (req.url === "/" && req.method === "GET") {
// Configurar el tipo de contenido como texto plano
res.setHeader("Content-Type", "text/plain");
// Enviar respuesta simple para la página de inicio
res.end("Inicio del servidor");
}
// ==========================================================================
// RUTA 2: Endpoint de API - Solo acepta POST con datos JSON
// ==========================================================================
else if (req.url === "/api" && req.method === "POST") {
// Variable para acumular los datos del cuerpo de la solicitud
let cuerpo = "";
// MANEJO DE DATOS EN STREAM:
// Las solicitudes POST pueden contener datos en el cuerpo
// Estos datos pueden llegar en múltiples fragmentos (chunks)
// Evento 'data': Se dispara por cada fragmento de datos recibido
req.on("data", (parte) => {
// 'parte' es un Buffer con un trozo de los datos
// Concatenamos cada parte a la variable 'cuerpo'
cuerpo += parte;
// EJEMPLO: Si se envía '{"usuario":"ana","edad":25}'
// Podría llegar: parte1 = '{"usuario":"ana"'
// parte2 = ',"edad":25}'
});
// Evento 'end': Se dispara cuando se han recibido TODOS los datos
req.on("end", () => {
try {
// Intentar parsear el cuerpo completo como JSON
// JSON.parse() convierte string JSON a objeto JavaScript
const datos = JSON.parse(cuerpo);
// Configurar respuesta como JSON
res.setHeader("Content-Type", "application/json");
// Enviar respuesta de confirmación con eco de los datos recibidos
res.end(
JSON.stringify({
recibido: true, // Confirmación de recepción
cuerpo: datos, // Eco de los datos enviados por el cliente
})
);
} catch (error) {
// Manejar error si el JSON es inválido
res.statusCode = 400; // Bad Request
res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify({
error: "JSON inválido",
mensaje: "El cuerpo de la solicitud no contiene JSON válido",
})
);
}
});
// Manejar errores en la recepción de datos
req.on("error", (error) => {
res.statusCode = 500; // Internal Server Error
res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify({
error: "Error al recibir datos",
mensaje: error.message,
})
);
});
}
// ==========================================================================
// MANEJO DE RUTAS NO ENCONTRADAS
// ==========================================================================
else {
// Cualquier otra combinación de ruta/método devuelve 404
res.statusCode = 404; // Not Found
res.end("No encontrado");
}
});
// Iniciar el servidor en el puerto 3000
server.listen(3000, () => {
console.log("Servidor ejecutándose en http://localhost:3000");
console.log("Endpoints disponibles:");
console.log(" GET http://localhost:3000/");
console.log(" POST http://localhost:3000/api");
});
Análisis detallado de las rutas y métodos:
Resumen de endpoints disponibles:
| Ruta | Método | Descripción | Content-Type |
|---|---|---|---|
/ | GET | Página de inicio | text/plain |
/api | POST | API para recibir datos JSON | application/json |
| Cualquier otra | Cualquiera | Error 404 | text/plain |
Comportamiento específico por ruta:
1. GET /
// SOLO funciona con:
// - URL exacta: "/"
// - Método exacto: "GET"
// Ejemplos que NO funcionarían:
// POST / → 404
// GET /home → 404
// GET /?param=123 → 404 (req.url sería "/?param=123")
2. POST /api
// SOLO funciona con:
// - URL exacta: "/api"
// - Método exacto: "POST"
// Características:
// - Espera datos JSON en el cuerpo
// - Devuelve eco de los datos recibidos
// - Maneja errores de JSON malformado
Cómo probar cada endpoint:
Probar GET / (página de inicio):
# Usando curl
curl <http://localhost:3000/>
# Usando navegador
# Visitar: <http://localhost:3000/>
Probar POST /api (endpoint de API):
# Usando curl
curl -X POST <http://localhost:3000/api> \\
-H "Content-Type: application/json" \\
-d '{"nombre":"Carlos","edad":30,"activo":true}'
# Respuesta esperada:
# {"recibido":true,"cuerpo":{"nombre":"Carlos","edad":30,"activo":true}}
Probar con JavaScript/fetch:
// Probar POST /api
fetch("<http://localhost:3000/api>", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
producto: "Teclado",
precio: 45.99,
categorias: ["electrónica", "oficina"],
}),
})
.then((response) => response.json())
.then((data) => console.log(data));
Casos de error que maneja:
1. JSON inválido en POST /api:
curl -X POST <http://localhost:3000/api> \\
-H "Content-Type: application/json" \\
-d '{"nombre": "Maria' # JSON malformado (falta llave)
# Respuesta: {"error":"JSON inválido","mensaje":"..."}
2. Rutas no existentes:
# Cualquier otra combinación devuelve 404
curl <http://localhost:3000/otra-ruta>
curl -X POST <http://localhost:3000/>
curl -X GET <http://localhost:3000/api>
Mejoras potenciales:
// MEJORA: Agregar manejo de parámetros de query
if (req.url === "/" && req.method === "GET") {
res.setHeader("Content-Type", "text/plain");
res.end("Inicio del servidor");
}
// MEJORA: Agregar más métodos HTTP
else if (req.url === "/api" && req.method === "GET") {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ mensaje: "API GET funcionando" }));
}
// MEJORA: Manejar CORS para peticiones desde navegadores
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST");
Este código representa un servidor web básico pero completo que demuestra el concepto de enrutamiento (routing) y manejo de diferentes métodos HTTP, sentando las bases para construir APIs REST más complejas.
Proyecto guiado completo: Servidor Node nativo con rutas, POST JSON y archivos estáticos
El objetivo de este proyecto es crear un servidor web real utilizando únicamente las herramientas nativas de Node, sin Express ni frameworks. Para ello se combinarán:
- rutas manuales
- manejo de peticiones GET y POST
- lectura del body en JSON
- respuesta en diferentes formatos
- servir archivos estáticos (HTML, CSS, JS)
- organización mínima del código
Este proyecto es perfecto para entender realmente cómo funciona Node por dentro antes de trabajar con frameworks.
1. Estructura inicial del proyecto
Para que sea lo más claro posible, se usará esta estructura:
proyecto-node/
server.js
public/
index.html
styles.css
app.js
La carpeta public actuará como raíz de archivos estáticos.
2. Crear el HTML básico
public/index.html:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>Servidor Node Nativo</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<h1>Servidor Node Nativo</h1>
<p>Este HTML está servido directamente desde Node.</p>
<form id="formulario">
<input type="text" id="nombre" placeholder="Introduce tu nombre" />
<button type="submit">Enviar</button>
</form>
<p id="respuesta"></p>
<script src="/app.js"></script>
</body>
</html>
3. Crear el JavaScript del cliente
public/app.js:
// Obtener referencias a los elementos del DOM
// document.getElementById() busca elementos por su atributo id
const formulario = document.getElementById("formulario");
const salida = document.getElementById("respuesta");
// Agregar un event listener para el evento 'submit' del formulario
// El evento submit se dispara cuando el usuario envía el formulario
formulario.addEventListener("submit", async (e) => {
// Prevenir el comportamiento por defecto del formulario
// Sin esto, la página se recargaría al enviar el formulario
e.preventDefault();
// Obtener el valor del campo de entrada con id "nombre"
// .value contiene el texto que el usuario escribió en el input
const nombre = document.getElementById("nombre").value;
// ==========================================================================
// ENVIAR DATOS AL SERVIDOR USANDO fetch()
// ==========================================================================
// fetch() es la API moderna para hacer peticiones HTTP desde JavaScript
// await pausa la ejecución hasta que la promesa se resuelva
const res = await fetch("/api/saludo", {
method: "POST", // Método HTTP: POST para enviar datos
headers: {
"Content-Type": "application/json", // Indicar que enviamos JSON
},
body: JSON.stringify({ nombre }), // Convertir objeto a string JSON
// Equivale a: JSON.stringify({ nombre: nombre })
});
// ==========================================================================
// PROCESAR LA RESPUESTA DEL SERVIDOR
// ==========================================================================
// res.json() parsea la respuesta como JSON
// Esto retorna otra promesa, por eso usamos await
const data = await res.json();
// Actualizar el contenido del elemento de salida con el mensaje recibido
// textContent establece el texto visible del elemento
salida.textContent = data.mensaje;
});
Esto enviará un POST a nuestro servidor nativo.
4. Crear un CSS mínimo
public/styles.css:
body {
font-family: Arial, sans-serif;
padding: 20px;
}
input,
button {
padding: 6px;
margin-top: 10px;
}
5. Crear el servidor Node nativo
Aquí está el archivo principal completo con todas las características explicadas.
server.js:
// Importar módulos necesarios de Node.js
import http from "node:http";
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import url from "node:url";
// Configuración del puerto del servidor
const PORT = 3000;
// Obtener el directorio actual del módulo (equivalente a __dirname en CommonJS)
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
// Función para servir archivos estáticos (HTML, CSS, JS, etc.)
async function servirArchivo(req, res) {
// Construir la ruta completa del archivo solicitado
// Si la URL es "/", servir index.html por defecto
const rutaArchivo = path.join(
__dirname,
"public",
req.url === "/" ? "index.html" : req.url
);
try {
// Verificar que el archivo existe antes de intentar leerlo
await stat(rutaArchivo);
// Leer el contenido del archivo
const contenido = await readFile(rutaArchivo);
// Determinar el tipo de contenido basado en la extensión del archivo
if (req.url.endsWith(".html")) res.setHeader("Content-Type", "text/html");
if (req.url.endsWith(".css")) res.setHeader("Content-Type", "text/css");
if (req.url.endsWith(".js"))
res.setHeader("Content-Type", "application/javascript");
// Enviar respuesta exitosa con el contenido del archivo
res.statusCode = 200;
res.end(contenido);
} catch {
// Si hay error (archivo no existe), enviar 404
res.statusCode = 404;
res.end("Archivo no encontrado");
}
}
// Función para leer y parsear el cuerpo de las solicitudes POST en formato JSON
function leerBodyJSON(req) {
return new Promise((resolve) => {
let cuerpo = "";
// Acumular los chunks de datos recibidos
req.on("data", (chunk) => (cuerpo += chunk));
// Cuando termina la recepción, parsear el JSON completo
req.on("end", () => resolve(JSON.parse(cuerpo)));
});
}
// Crear el servidor HTTP principal
const server = http.createServer(async (req, res) => {
// Ruta de API para saludos - solo acepta POST
if (req.url === "/api/saludo" && req.method === "POST") {
// Leer y parsear el cuerpo de la solicitud
const datos = await leerBodyJSON(req);
// Configurar respuesta como JSON
res.setHeader("Content-Type", "application/json");
// Enviar respuesta con saludo personalizado
return res.end(
JSON.stringify({
ok: true,
mensaje: `Hola ${datos.nombre}, el servidor Node te saluda`,
})
);
}
// Para cualquier otra ruta, servir archivos estáticos
await servirArchivo(req, res);
});
// Iniciar el servidor
server.listen(PORT, () => {
console.log(`Servidor escuchando en http://localhost:${PORT}`);
});
6. Explicación paso a paso
Servir archivos estáticos
El servidor analiza la URL y trata de leer el archivo correspondiente dentro de la carpeta public. Esto enseña:
- cómo leer archivos con
fs/promises - cómo obtener rutas absolutas usando
pathyurl - cómo construir un mini sistema de archivos estáticos como hace Express
Detectar un POST JSON
El servidor detecta manualmente:
if (req.url === "/api/saludo" && req.method === "POST")
Después lee el body usando eventos data y end. Esto permite que el alumno entienda cómo Node gestiona streams y fragmentos de datos.
Responder con JSON
El servidor devuelve JSON de forma manual usando:
JSON.stringify(...)
Esto muestra claramente que Node nativo no tiene métodos como res.json o res.send típicos de Express.
Un único servidor, dos comportamientos
El mismo servidor gestiona:
- rutas API
- archivos estáticos
- errores
Esto resume el funcionamiento básico de cualquier backend.
7. Resultado final
Al abrir en el navegador:
http://localhost:3000
Se verá la página, y al enviar el formulario se producirá un POST real, manejado por Node nativo, que responde con JSON dinámico.
Nueva estructura del proyecto. Separación por módulos.
Partimos de algo así:
proyecto-node/
server-http.js
server-https.js
router.js
static.js
utils.js
db/
usuarios.json
views/
layout.html
home.html
public/
app.js
styles.css
Notas rápidas:
server-http.jsyserver-https.jsusan el mismo router.router.jsdecide qué hacer con cada petición.static.jssirve archivos depublic.utils.jscentraliza utilidades: MIME types, parseo de URL, lectura/escritura JSON, motor de plantillas.db/usuarios.jsonsimula una pequeña base de datos.views/contiene las plantillas HTML del mini motor de plantillas.
Asumo que tienes "type": "module" en package.json para usar ES modules.
utils.js
Utilidades: MIME, URL, JSON y motor de plantillas simple
utils.js:
// utils.js
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import url from "node:url";
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
// Resolver rutas relativas al proyecto
export function resolvePath(...segmentos) {
return path.join(__dirname, ...segmentos);
}
// Detección de Content-Type según extensión
export function getContentType(rutaArchivo) {
const ext = path.extname(rutaArchivo).toLowerCase();
const mapa = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".svg": "image/svg+xml",
".ico": "image/x-icon",
};
return mapa[ext] || "application/octet-stream";
}
// Parseo de la URL con query strings
export function parseRequestUrl(req) {
const base = `http://${req.headers.host}`;
const urlObj = new URL(req.url, base);
return {
pathname: urlObj.pathname, // /api/usuarios
searchParams: urlObj.searchParams, // URLSearchParams
};
}
// Leer JSON de un archivo
export async function readJson(relativePath) {
const ruta = resolvePath(relativePath);
const contenido = await readFile(ruta, "utf-8");
return JSON.parse(contenido);
}
// Escribir JSON en un archivo
export async function writeJson(relativePath, data) {
const ruta = resolvePath(relativePath);
const texto = JSON.stringify(data, null, 2);
await writeFile(ruta, texto, "utf-8");
}
// Motor de plantillas muy básico
// layout.html contiene {{content}} donde se incrusta la vista
// cada plantilla puede tener {{titulo}}, {{mensaje}}, etc.
export async function renderTemplate(viewName, data = {}) {
const layoutPath = resolvePath("views", "layout.html");
const viewPath = resolvePath("views", `${viewName}.html`);
let layout = await readFile(layoutPath, "utf-8");
let vista = await readFile(viewPath, "utf-8");
// Reemplazar variables {{clave}} en la vista
for (const [clave, valor] of Object.entries(data)) {
const token = new RegExp(`{{\\s*${clave}\\s*}}`, "g");
vista = vista.replace(token, String(valor));
}
// Insertar la vista dentro del layout
layout = layout.replace("{{content}}", vista);
return layout;
}
static.js
Servidor de archivos estáticos con tipos MIME y cabeceras
static.js:
// static.js
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import { resolvePath, getContentType } from "./utils.js";
// Servir archivos de la carpeta public
export async function serveStatic(req, res) {
// Si pide "/" servimos public/index.html (o home dinámico desde router)
// Normalizar la URL: para raíz servir index.html, para otras URLs usar la ruta solicitada
const urlPath = req.url === "/" ? "/index.html" : req.url;
// Resolver la ruta completa del archivo:
// - Usa resolvePath para construir la ruta absoluta
// - path.normalize elimina rutas relativas como ".." por seguridad
// - replace(/^\/+/, "") remueve barras iniciales para evitar rutas absolutas
const rutaArchivo = resolvePath(
"public",
path.normalize(urlPath).replace(/^\/+/, "")
);
try {
await stat(rutaArchivo); // Comprueba que existe el archivo
// Leer el contenido del archivo del sistema de archivos
const contenido = await readFile(rutaArchivo);
// Determinar el tipo MIME basado en la extensión del archivo
const contentType = getContentType(rutaArchivo);
// Configurar respuesta exitosa
res.statusCode = 200;
res.setHeader("Content-Type", contentType);
// Ejemplo de cabeceras adicionales
res.setHeader("X-Powered-By", "Node.js nativo");
// Cabecera de cache para optimización (cache por 60 segundos)
res.setHeader("Cache-Control", "public, max-age=60");
// Enviar el contenido del archivo
res.end(contenido);
return true; // Indica que se ha servido exitosamente
} catch {
return false; // No encontrado en public - permite que otro middleware maneje la solicitud
}
}
db/usuarios.json
Base de datos en JSON
db/usuarios.json:
[
{ "id": 1, "nombre": "Ana" },
{ "id": 2, "nombre": "Luis" },
{ "id": 3, "nombre": "Marta" }
]
Puedes ir añadiendo usuarios vía API.
views/layout.html y home.html
Mini motor de plantillas en acción
views/layout.html:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>{{titulo}}</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<header>
<h1>{{titulo}}</h1>
</header>
<main>{{content}}</main>
<footer>
<small>Servidor Node nativo con plantillas simples</small>
</footer>
<script src="/app.js"></script>
</body>
</html>
views/home.html:
<section>
<p>{{mensaje}}</p>
<h2>Lista de usuarios (desde JSON)</h2>
<ul>
{{listaUsuarios}}
</ul>
<h2>Crear usuario</h2>
<form id="form-usuario">
<input type="text" id="nombre" placeholder="Nombre" />
<button type="submit">Guardar</button>
</form>
<p id="respuesta"></p>
</section>
Observa que aquí dejamos un hueco {{listaUsuarios}} que rellenaremos montando HTML desde el router.
public/app.js y styles.css
JavaScript del cliente para la API y algo de estilo
public/app.js:
// Manejo del formulario para crear usuario vía fetch POST JSON
// Obtener referencias a los elementos del DOM
const form = document.getElementById("form-usuario");
const salida = document.getElementById("respuesta");
// Verificar si el formulario existe en la página actual
if (form) {
// Agregar event listener para el evento submit del formulario
form.addEventListener("submit", async (e) => {
e.preventDefault(); // Prevenir el envío tradicional del formulario (recarga de página)
// Obtener y limpiar el valor del campo nombre (eliminar espacios en blanco)
const nombre = document.getElementById("nombre").value.trim();
// Validación básica del campo nombre
if (!nombre) {
salida.textContent = "El nombre es obligatorio";
return; // Detener la ejecución si no hay nombre
}
// Enviar datos al servidor mediante fetch API
const res = await fetch("/api/usuarios", {
method: "POST", // Método HTTP para crear recurso
headers: { "Content-Type": "application/json" }, // Especificar que enviamos JSON
body: JSON.stringify({ nombre }), // Convertir objeto a string JSON
});
// Procesar la respuesta del servidor (convertir de JSON a objeto)
const data = await res.json();
// Mostrar mensaje de respuesta (usar mensaje del servidor o uno por defecto)
salida.textContent = data.mensaje || "Usuario creado";
// Recargar la página para ver la lista actualizada (enfoque simple)
// Esto asegura que se muestren los datos actualizados del servidor
window.location.reload();
});
}
public/styles.css (puedes reutilizar el anterior y ampliarlo):
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
margin: 0;
padding: 20px;
background: #f7f7f7;
}
header,
main,
footer {
max-width: 800px;
margin: 0 auto;
}
header h1 {
margin-bottom: 10px;
}
section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
}
input,
button {
padding: 6px 10px;
margin-top: 10px;
}
router.js
Sistema de rutas más limpio + uso de JSON, plantillas y query strings
router.js:
// router.js
import {
parseRequestUrl,
readJson,
writeJson,
renderTemplate,
} from "./utils.js";
import { serveStatic } from "./static.js";
// Leer body JSON en POST
function readBodyJson(req) {
return new Promise((resolve, reject) => {
let cuerpo = "";
// Acumular chunks de datos recibidos en el cuerpo de la solicitud
req.on("data", (chunk) => {
cuerpo += chunk;
});
// Cuando termina la recepción de datos, procesar el cuerpo completo
req.on("end", () => {
try {
// Parsear JSON si hay contenido, sino objeto vacío
const data = cuerpo ? JSON.parse(cuerpo) : {};
resolve(data);
} catch (err) {
// Rechazar la promesa si hay error en el parseo JSON
reject(err);
}
});
});
}
// Generar lista HTML de usuarios para la plantilla
function renderListaUsuarios(usuarios) {
// Manejar caso cuando no hay usuarios
if (!usuarios.length) {
return "<li>No hay usuarios registrados</li>";
}
// Convertir array de usuarios a lista HTML
return usuarios.map((u) => `<li>${u.id} - ${u.nombre}</li>`).join("");
}
// Router principal - maneja todas las solicitudes entrantes
export async function handleRequest(req, res) {
// Parsear URL para obtener pathname y parámetros de búsqueda
const { pathname, searchParams } = parseRequestUrl(req);
const metodo = req.method;
// Cabeceras comunes para todas las respuestas
res.setHeader("X-Powered-By", "Node nativo");
res.setHeader("Access-Control-Allow-Origin", "*"); // ejemplo CORS simple
// ========== RUTAS API ==========
// GET /api/usuarios - Obtener lista de usuarios (con filtro opcional)
if (pathname === "/api/usuarios" && metodo === "GET") {
const usuarios = await readJson("db/usuarios.json");
// Filtro opcional por ?nombre= en query string
const filtroNombre = searchParams.get("nombre");
let filtrados = usuarios;
// Aplicar filtro si se especificó parámetro nombre
if (filtroNombre) {
filtrados = usuarios.filter((u) =>
u.nombre.toLowerCase().includes(filtroNombre.toLowerCase())
);
}
res.statusCode = 200;
res.setHeader("Content-Type", "application/json; charset=utf-8");
return res.end(JSON.stringify(filtrados));
}
// POST /api/usuarios - Crear nuevo usuario
if (pathname === "/api/usuarios" && metodo === "POST") {
try {
// Leer y parsear cuerpo de la solicitud
const body = await readBodyJson(req);
const usuarios = await readJson("db/usuarios.json");
// Generar nuevo ID (último ID + 1, o 1 si no hay usuarios)
const nuevoId = usuarios.length
? usuarios[usuarios.length - 1].id + 1
: 1;
const nuevoUsuario = { id: nuevoId, nombre: body.nombre };
// Agregar usuario y guardar en base de datos
usuarios.push(nuevoUsuario);
await writeJson("db/usuarios.json", usuarios);
// Responder con éxito 201 (Created)
res.statusCode = 201;
res.setHeader("Content-Type", "application/json; charset=utf-8");
return res.end(
JSON.stringify({
ok: true,
mensaje: `Usuario creado con id ${nuevoId}`,
usuario: nuevoUsuario,
})
);
} catch {
// Error 400 si el JSON es inválido
res.statusCode = 400;
res.setHeader("Content-Type", "application/json; charset=utf-8");
return res.end(JSON.stringify({ ok: false, error: "JSON inválido" }));
}
}
// ========== RUTAS VISTAS ==========
// Página principal dinámica con plantillas
if (pathname === "/" && metodo === "GET") {
const usuarios = await readJson("db/usuarios.json");
const listaUsuarios = renderListaUsuarios(usuarios);
// Renderizar plantilla home con datos dinámicos
const html = await renderTemplate("home", {
titulo: "Servidor Node nativo",
mensaje: "Esta página se ha generado con un mini motor de plantillas.",
listaUsuarios,
});
res.statusCode = 200;
res.setHeader("Content-Type", "text/html; charset=utf-8");
return res.end(html);
}
// ========== ARCHIVOS ESTÁTICOS ==========
// Si no es una ruta conocida, intentar servir archivo estático
const servido = await serveStatic(req, res);
if (servido) return;
// ========== RUTA NO ENCONTRADA ==========
// Si no se ha servido nada: 404 Not Found
res.statusCode = 404;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Ruta no encontrada");
}
Aquí ya tienes:
- sistema de rutas algo más declarativo
- parseo de query strings con
URLSearchParams - lectura y escritura de JSON como “DB”
- plantillas HTML con valores dinámicos
- cabeceras comunes y CORS sencillo
server-http.js
Servidor HTTP que delega todo al router
server-http.js:
// server-http.js
import http from "node:http";
import { handleRequest } from "./router.js";
// Definir puerto donde escuchará el servidor
const PORT = 3000;
// Crear servidor HTTP nativo de Node.js
const server = http.createServer(async (req, res) => {
try {
// Delegar el manejo de la solicitud al router principal
// await para esperar a que se complete el procesamiento de la solicitud
await handleRequest(req, res);
} catch (err) {
// Manejo centralizado de errores - captura cualquier error no controlado
console.error("Error en la petición:", err);
// Responder con error 500 (Internal Server Error) al cliente
res.statusCode = 500;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Error interno del servidor");
}
});
// Iniciar el servidor y hacer que escuche en el puerto especificado
server.listen(PORT, () => {
// Callback que se ejecuta cuando el servidor está listo y escuchando
console.log(`HTTP escuchando en http://localhost:${PORT}`);
});
server-https.js
Versión HTTPS usando los mismos handlers
Necesitas generar un certificado autofirmado para pruebas (por ejemplo con OpenSSL) y guardarlo como cert/clave.key y cert/cert.crt.
server-https.js:
// server-https.js
import https from "node:https";
import { readFileSync } from "node:fs";
import { handleRequest } from "./router.js";
// Definir puerto para HTTPS (puerto estándar para HTTPS es 443, pero en desarrollo usamos 3443)
const PORT = 3443;
// Configuración de opciones SSL/TLS necesarias para HTTPS
const options = {
key: readFileSync("./cert/clave.key"), // Leer clave privada del servidor
cert: readFileSync("./cert/cert.crt"), // Leer certificado público del servidor
};
// Crear servidor HTTPS con las opciones de certificado
const server = https.createServer(options, async (req, res) => {
try {
// Delegar el manejo de la solicitud al mismo router que HTTP
// Toda la comunicación aquí está cifrada gracias a HTTPS
await handleRequest(req, res);
} catch (err) {
// Manejo centralizado de errores para HTTPS
console.error("Error en la petición HTTPS:", err);
// Responder con error 500 (Internal Server Error)
res.statusCode = 500;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Error interno del servidor");
}
});
// Iniciar servidor HTTPS en el puerto especificado
server.listen(PORT, () => {
// Callback que se ejecuta cuando el servidor HTTPS está listo
console.log(`HTTPS escuchando en https://localhost:${PORT}`);
});
En el navegador tendrás que aceptar la excepción de seguridad al usar un certificado autofirmado.
Con esto ya tienes:
- Separación clara en módulos (
router,static,utils) - Parser de query strings con
URLSearchParams - Mini motor de plantillas sin librerías
- “base de datos” JSON persistente
- Versión HTTP y HTTPS reutilizando el mismo router
- Sistema de rutas limpio y extensible
- Manejo más avanzado de cabeceras y tipos MIME
Ahora, vamos a refactorizar el proyecto para introducir controladores y luego añadir un sistema de tests muy sencillo con Node nativo, sin Jest ni librerías externas.
Voy a darte todo listo para sustituir/añadir archivos.
Nueva estructura con controladores y tests
Partimos de algo así:
proyecto-node/
server-http.js
server-https.js
router.js
static.js
utils.js
controllers/
homeController.js
usersController.js
db/
usuarios.json
views/
layout.html
home.html
public/
app.js
styles.css
tests/
users.test.js
test-runner.js
controllers/contiene la lógica de cada “zona” (home, usuarios, etc.).router.jsahora solo decide qué controlador llamar.tests/contiene tests muy básicos.test-runner.jses un mini “runner” casero para ejecutarlos.
Refactor: utils.js (pequeño ajuste)
Añadimos una función genérica para leer body JSON, que podrán usar los controladores:
// utils.js
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import url from "node:url";
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
// Resolver rutas relativas al proyecto
export function resolvePath(...segmentos) {
return path.join(__dirname, ...segmentos);
}
// Detección de Content-Type según extensión
export function getContentType(rutaArchivo) {
const ext = path.extname(rutaArchivo).toLowerCase();
const mapa = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".svg": "image/svg+xml",
".ico": "image/x-icon",
};
return mapa[ext] || "application/octet-stream";
}
// Parseo de la URL con query strings
export function parseRequestUrl(req) {
const base = `http://${req.headers.host}`;
const urlObj = new URL(req.url, base);
return {
pathname: urlObj.pathname,
searchParams: urlObj.searchParams,
};
}
// Leer JSON de un archivo
export async function readJson(relativePath) {
const ruta = resolvePath(relativePath);
const contenido = await readFile(ruta, "utf-8");
return JSON.parse(contenido);
}
// Escribir JSON en un archivo
export async function writeJson(relativePath, data) {
const ruta = resolvePath(relativePath);
const texto = JSON.stringify(data, null, 2);
await writeFile(ruta, texto, "utf-8");
}
// Leer body como JSON
export function readBodyJson(req) {
return new Promise((resolve, reject) => {
let cuerpo = "";
req.on("data", (chunk) => {
cuerpo += chunk;
});
req.on("end", () => {
try {
const data = cuerpo ? JSON.parse(cuerpo) : {};
resolve(data);
} catch (err) {
reject(err);
}
});
});
}
// Motor de plantillas muy básico
export async function renderTemplate(viewName, data = {}) {
const layoutPath = resolvePath("views", "layout.html");
const viewPath = resolvePath("views", `${viewName}.html`);
let layout = await readFile(layoutPath, "utf-8");
let vista = await readFile(viewPath, "utf-8");
for (const [clave, valor] of Object.entries(data)) {
const token = new RegExp(`{{\\s*${clave}\\s*}}`, "g");
vista = vista.replace(token, String(valor));
}
layout = layout.replace("{{content}}", vista);
return layout;
}
controllers/usersController.js
Controlador de usuarios + funciones “puras” probables para testear.
// controllers/usersController.js
import { readJson, writeJson, readBodyJson } from "../utils.js";
// Función pura: genera la lista HTML de usuarios (ideal para tests)
export function buildUsersListHtml(usuarios) {
if (!Array.isArray(usuarios) || usuarios.length === 0) {
return "<li>No hay usuarios registrados</li>";
}
return usuarios.map((u) => `<li>${u.id} - ${u.nombre}</li>`).join("");
}
// Lógica de dominio: obtener usuarios (con filtro opcional)
export async function getUsers({ nombre }) {
const usuarios = await readJson("db/usuarios.json");
if (!nombre) return usuarios;
const filtro = nombre.toLowerCase();
return usuarios.filter((u) => u.nombre.toLowerCase().includes(filtro));
}
// Lógica de dominio: crear usuario
export async function createUser({ nombre }) {
if (!nombre || typeof nombre !== "string" || !nombre.trim()) {
throw new Error("Nombre inválido");
}
const usuarios = await readJson("db/usuarios.json");
const nuevoId = usuarios.length ? usuarios[usuarios.length - 1].id + 1 : 1;
const nuevoUsuario = { id: nuevoId, nombre: nombre.trim() };
usuarios.push(nuevoUsuario);
await writeJson("db/usuarios.json", usuarios);
return nuevoUsuario;
}
// Controlador HTTP: GET /api/usuarios
export async function handleGetUsers(req, res, searchParams) {
const nombre = searchParams.get("nombre");
const usuarios = await getUsers({ nombre });
res.statusCode = 200;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(usuarios));
}
// Controlador HTTP: POST /api/usuarios
export async function handlePostUsers(req, res) {
try {
const body = await readBodyJson(req);
const nuevoUsuario = await createUser({ nombre: body.nombre });
res.statusCode = 201;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(
JSON.stringify({
ok: true,
mensaje: `Usuario creado con id ${nuevoUsuario.id}`,
usuario: nuevoUsuario,
})
);
} catch (err) {
res.statusCode = 400;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify({ ok: false, error: err.message }));
}
}
controllers/homeController.js
Controlador para la página principal HTML:
// controllers/homeController.js
import { readJson, renderTemplate } from "../utils.js";
import { buildUsersListHtml } from "./usersController.js";
export async function handleHome(req, res) {
const usuarios = await readJson("db/usuarios.json");
const listaUsuarios = buildUsersListHtml(usuarios);
const html = await renderTemplate("home", {
titulo: "Servidor Node nativo",
mensaje: "Esta página se ha generado con un mini motor de plantillas.",
listaUsuarios,
});
res.statusCode = 200;
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(html);
}
static.js (igual que antes, encaja con router nuevo)
Por si acaso lo recuerdo tal cual:
// static.js
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import { resolvePath, getContentType } from "./utils.js";
export async function serveStatic(req, res) {
const urlPath = req.url === "/" ? "/index.html" : req.url;
const rutaArchivo = resolvePath(
"public",
path.normalize(urlPath).replace(/^\/+/, "")
);
try {
await stat(rutaArchivo);
const contenido = await readFile(rutaArchivo);
const contentType = getContentType(rutaArchivo);
res.statusCode = 200;
res.setHeader("Content-Type", contentType);
res.setHeader("X-Powered-By", "Node.js nativo");
res.setHeader("Cache-Control", "public, max-age=60");
res.end(contenido);
return true;
} catch {
return false;
}
}
router.js simplificado
El router ahora solo decide qué controlador llamar:
// router.js
import { parseRequestUrl } from "./utils.js";
import { serveStatic } from "./static.js";
import { handleHome } from "./controllers/homeController.js";
import {
handleGetUsers,
handlePostUsers,
} from "./controllers/usersController.js";
export async function handleRequest(req, res) {
const { pathname, searchParams } = parseRequestUrl(req);
const metodo = req.method;
// Cabeceras comunes
res.setHeader("X-Powered-By", "Node nativo");
res.setHeader("Access-Control-Allow-Origin", "*");
// Rutas API
if (pathname === "/api/usuarios" && metodo === "GET") {
return handleGetUsers(req, res, searchParams);
}
if (pathname === "/api/usuarios" && metodo === "POST") {
return handlePostUsers(req, res);
}
// Página principal dinámica
if (pathname === "/" && metodo === "GET") {
return handleHome(req, res);
}
// Intentar servir estáticos
const servido = await serveStatic(req, res);
if (servido) return;
// 404
res.statusCode = 404;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Ruta no encontrada");
}
server-http.js y server-https.js que ya tenías siguen siendo válidos, solo que ahora importan este router refactorizado.
Mini sistema de tests nativo
7.1. tests/users.test.js
Un test muy sencillo para comprobar la función pura buildUsersListHtml y, de paso, un trocito de lógica de creación de usuarios.
// tests/users.test.js
import { strict as assert } from "node:assert";
import {
buildUsersListHtml,
getUsers,
createUser,
} from "../controllers/usersController.js";
// Mini función casera para definir tests
async function test(nombre, fn) {
try {
await fn();
console.log(`✓ ${nombre}`);
} catch (err) {
console.error(`✗ ${nombre}`);
console.error(" Error:", err.message);
}
}
// Tests sincrónicos sobre función pura
await test("buildUsersListHtml devuelve mensaje si no hay usuarios", () => {
const html = buildUsersListHtml([]);
assert.ok(html.includes("No hay usuarios"));
});
await test("buildUsersListHtml dibuja <li> por cada usuario", () => {
const html = buildUsersListHtml([
{ id: 1, nombre: "Ana" },
{ id: 2, nombre: "Luis" },
]);
const numLi = (html.match(/<li>/g) || []).length;
assert.equal(numLi, 2);
});
// Tests asíncronos muy básicos sobre lógica de dominio
await test("getUsers devuelve un array", async () => {
const usuarios = await getUsers({ nombre: null });
assert.ok(Array.isArray(usuarios));
});
await test("createUser lanza error si el nombre es vacío", async () => {
let lanzoError = false;
try {
await createUser({ nombre: " " });
} catch {
lanzoError = true;
}
assert.equal(lanzoError, true);
});
Notas:
- No usamos Jest ni node:test.
- Usamos
assertnativo y nuestra propia funcióntest. - Los tests se centran en funciones puras o de dominio, no en peticiones HTTP completas (eso lo podrías ampliar después).
7.2. test-runner.js
Archivo para ejecutar todos los tests:
// test-runner.js
import "./tests/users.test.js";
// Si creas más tests, los vas importando aquí:
// import "./tests/otraCosa.test.js";
Con "type": "module" en package.json, basta con ejecutar:
node test-runner.js
(en Windows es lo mismo en PowerShell o CMD).
Verás algo como:
✓ buildUsersListHtml devuelve mensaje si no hay usuarios
✓ buildUsersListHtml dibuja <li> por cada usuario
✓ getUsers devuelve un array
✓ createUser lanza error si el nombre es vacío
Con esto los alumnos ven que:
- Se pueden probar partes del backend sin frameworks
- Separar lógica en funciones puras facilita el testing
- No hace falta nada más que Node + assert para empezar a introducir buenas prácticas