El app-minimo.js de la lección anterior cabía en diez líneas y arrancaba con aplicacion.listen(3000). Funciona, pero no se puede probar, no se puede apagar de forma ordenada, no se puede configurar por entorno y no tiene sitio donde crecer. En esta lección lo convertimos en el esqueleto real de Escena Viva: una aplicación que se crea en un sitio y se arranca en otro, con la configuración centralizada y validada, los ajustes del framework elegidos a conciencia y una estructura de carpetas que aguante los seis módulos que quedan de curso.
Contenido
- La separación clave: crear la aplicación y arrancarla
src/app.js: la factoríacrearAplicacion()src/servidor.js: el arranque- Estructura de carpetas de Escena Viva
- Ajustes de la aplicación con
app.setyapp.get - Configuración por entorno:
dotenvysrc/config/index.js - Montar la aplicación en un servidor
httppropio - Apagado ordenado con
SIGTERM app.localsfrente ares.locals- Una nota sobre motores de vistas
- La separación clave: crear la aplicación y arrancarla
Regla que no vamos a romper en el resto del curso:
Un módulo crea la aplicación y la devuelve. Otro módulo distinto la pone a escuchar.
Es decir: src/app.js exporta crearAplicacion(), que construye el objeto de Express con todas sus rutas y middleware y lo devuelve sin llamar a listen. Y src/servidor.js importa esa función, crea el servidor HTTP y lo pone a escuchar.
Parece un detalle de estilo. No lo es:
| Razón | Qué pasaría si mezclaras ambas cosas |
|---|---|
| Pruebas (M9) | supertest necesita la aplicación sin arrancar: le pasa la app y él levanta un servidor efímero en un puerto libre. Si app.js llama a listen, cada fichero de prueba intentaría ocupar el puerto 3000 y las pruebas fallarían en paralelo. |
| Apagado ordenado (M11) | Para cerrar bien necesitas la referencia al objeto Server, no a la app. Solo la tienes si el arranque es explícito. |
| Reutilización | Puedes montar varias instancias (API y métricas en puertos distintos) o anidarla con app.use('/api/v1', crearAplicacion()). |
Además, crearAplicacion() es una factoría, no un objeto exportado directamente: permite pasarle opciones y garantiza que cada prueba obtenga una aplicación limpia, sin estado compartido de la anterior.
src/app.js: la factoría crearAplicacion()
src/app.js: la factoría crearAplicacion()Esta es la primera versión. Irá creciendo en 06-03 (routers), 06-04 (middleware propio), 06-05 (middleware de terceros) y 06-07 (errores).
// src/app.js
const express = require('express');
const { configuracion } = require('./config/index.js');
/**
* Construye la aplicacion Express de Escena Viva.
* No arranca ningun servidor: solo devuelve la aplicacion configurada.
*/
function crearAplicacion(opciones = {}) {
const { servirEstaticos = true } = opciones;
const aplicacion = express();
// --- Ajustes del framework (apartado 5) ---
aplicacion.disable('x-powered-by');
aplicacion.set('trust proxy', configuracion.confiarEnProxy);
aplicacion.set('case sensitive routing', true);
aplicacion.set('json spaces', configuracion.esProduccion ? 0 : 2);
// --- Valores compartidos por toda la aplicacion (apartado 9) ---
aplicacion.locals.nombrePlataforma = 'Escena Viva';
aplicacion.locals.version = require('../package.json').version;
// --- Middleware integrado: lectura del cuerpo JSON y estaticos ---
aplicacion.use(express.json({ limit: configuracion.limiteCuerpo }));
if (servirEstaticos) {
aplicacion.use(express.static(configuracion.directorioPublico, { index: 'index.html' }));
}
// --- Rutas (se llenaran en 06-03) ---
aplicacion.get('/salud', (peticion, respuesta) => {
const { nombrePlataforma, version } = aplicacion.locals;
respuesta.json({ estado: 'ok', plataforma: nombrePlataforma, version });
});
return aplicacion;
}
module.exports = { crearAplicacion };Puntos que merece la pena señalar: no hay listen por ninguna parte (ese es el contrato); la función recibe opciones con valores por defecto, así que crearAplicacion() sin argumentos sigue funcionando; la configuración llega de un módulo, no de process.env esparcido por el fichero (apartado 6); y los diagnósticos no se imprimen aquí, porque app.js no habla por consola, solo construye.
src/servidor.js: el arranque
src/servidor.js: el arranque// src/servidor.js
const http = require('node:http');
const { crearAplicacion } = require('./app.js');
const { configuracion } = require('./config/index.js');
/** Crea el servidor HTTP y lo pone a escuchar. */
function arrancarServidor() {
// Creamos el servidor a mano en lugar de usar aplicacion.listen(...):
// asi conservamos la referencia al Server para cerrarlo (apartados 7 y 8).
const servidor = http.createServer(crearAplicacion());
const { puerto, host, entorno } = configuracion;
servidor.listen(puerto, host, () => {
console.error(`[escena-viva] entorno=${entorno} escuchando en http://${host}:${puerto}`);
});
registrarApagadoOrdenado(servidor);
return servidor;
}
module.exports = { arrancarServidor };
// El fichero es modulo y script a la vez.
if (require.main === module) arrancarServidor();La función registrarApagadoOrdenado la escribiremos en el apartado 8. Actualiza también los scripts del package.json, porque el punto de entrada ha cambiado respecto al módulo 4: "start": "node src/servidor.js" y "dev": "node --watch src/servidor.js".
- Estructura de carpetas de Escena Viva
Express no impone estructura. Esta es la nuestra, pensada para que cada fichero tenga una sola razón para cambiar.
escena-viva/
├── datos/ # eventos.json, ventas.csv (ya existia)
├── publico/ # index.html, estilos.css, app.js (ya existia)
├── src/
│ ├── app.js # crearAplicacion()
│ ├── servidor.js # arranque y apagado
│ ├── config/ # index.js (NUEVO) + rutas.js (ya existia)
│ ├── rutas/ # routers de Express (06-03)
│ ├── controladores/ # manejadores (req, res) (06-03)
│ ├── middleware/ # middleware propio (06-04)
│ ├── esquemas/ # validacion con zod (06-06)
│ ├── errores.js # jerarquia de errores (06-07)
│ ├── dominio/ # Evento, Sesion, GestorDeVentas (ya existia)
│ ├── servicios/ # cambio-divisas.js y logica de negocio
│ ├── catalogo-datos.js # acceso al JSON (ya existia)
│ ├── informes/ streams/ utiles/ y servidor/ (el artesanal del M4, referencia)
└── package.jsonResponsabilidades, para que nadie tenga dudas de dónde va cada cosa:
| Carpeta | Responsabilidad | Qué no debe contener |
|---|---|---|
rutas/ |
Declarar qué URL llama a qué controlador y qué middleware la protege | Lógica de negocio |
controladores/ |
Traducir HTTP a dominio: leer req, llamar a un servicio, responder |
Reglas de aforo, acceso a ficheros |
servicios/ |
Casos de uso: orquestar dominio y datos | Objetos req/res |
dominio/ |
Reglas de Escena Viva: aforo, estados, precios | Nada de HTTP ni de Express |
middleware/ |
Preocupaciones transversales: registro, identificador, caché | Reglas específicas de un endpoint |
esquemas/ y config/ |
Forma esperada de la entrada; lectura y validación del entorno | Consultas, respuestas ni lógica |
La regla que lo resume: el dominio no importa Express, y Express no importa fs. Si un fichero de dominio/ necesita require('express'), algo está mal colocado.
Advertencia contra la sobreingeniería
Esta estructura tiene sentido para una API que va a crecer durante seis módulos más. Para un proyecto de tres endpoints es exagerada. Señales de que te estás pasando: controladores que solo llaman a un servicio que solo llama a un repositorio que solo hace una línea (tres ficheros para una operación trivial), carpetas vacías "por si acaso", e interfaces y abstracciones para una única implementación que nunca va a tener otra.
Empieza por rutas/ + controladores/ y saca servicios/ cuando la lógica se repita en dos controladores, no antes. En Escena Viva sí llegaremos a ese punto, porque la venta de entradas se usa desde la API y desde los informes.
- Ajustes de la aplicación con
app.set y app.get
app.set y app.getExpress guarda ajustes en un diccionario interno. app.set(nombre, valor) escribe, app.get(nombre) lee (ojo: app.get con dos argumentos registra una ruta GET; con uno, lee un ajuste), y app.enable/app.disable son atajos para valores booleanos.
| Ajuste | Valor recomendado | Qué hace y por qué |
|---|---|---|
env |
process.env.NODE_ENV o development |
En producción se activa la caché de vistas y el manejador de errores deja de enviar la traza (06-07) |
x-powered-by |
desactivado | Express añade X-Powered-By: Express a cada respuesta: información gratuita para quien busca objetivos con una versión conocida. No es gran defensa, pero no cuesta nada |
trust proxy |
1 o la lista de proxies de confianza |
Detrás de Nginx o de un balanceador, req.ip es la IP del proxy y req.protocol es http aunque el cliente venga por HTTPS. Con este ajuste, Express lee X-Forwarded-For y X-Forwarded-Proto. Imprescindible para el limitador de peticiones de 06-05 y para el registro del módulo 11 |
json spaces |
2 en desarrollo, 0 en producción |
Indenta el JSON de res.json(). Cómodo al depurar con curl, desperdicio de ancho de banda en producción |
case sensitive routing |
true |
Sin él, /Eventos y /eventos son la misma ruta. Que dos URL distintas devuelvan lo mismo perjudica a la caché y al SEO |
strict routing |
false |
Con false, /eventos y /eventos/ son la misma ruta (comportamiento amable). Con true, son distintas. Nuestro normalizarRuta del módulo 4 hacía justo esto a mano |
query parser |
'simple' o 'extended' |
Cómo se interpretan las query strings anidadas (?filtro[sala]=Bóveda). 'simple' usa querystring del núcleo; 'extended' usa qs y admite anidamiento |
Desde un manejador puedes leerlos con aplicacion.get('env') o aplicacion.get('trust proxy'); de ese último dependen peticion.ip y peticion.protocol.
Cuidado con
trust proxy. Actívalo solo si realmente hay un proxy delante. Expuesto directamente a internet, cualquiera puede falsificar su IP enviando una cabeceraX-Forwarded-For, y tu limitador de peticiones deja de servir para nada. Por eso en nuestra configuración es un valor de entorno, no una constante.
- Configuración por entorno:
dotenv y src/config/index.js
dotenv y src/config/index.jsdotenv ya es dependencia desde el módulo 5. Su trabajo es cargar un fichero .env dentro de process.env. Nada más: no valida ni convierte tipos. Eso lo hacemos nosotros.
# .env (NO se sube al repositorio; en el repositorio va .env.ejemplo)
NODE_ENV=desarrollo
PUERTO=3000
LIMITE_CUERPO=100kb
CONFIAR_EN_PROXY=0
ORIGENES_PERMITIDOS=http://localhost:5173,http://127.0.0.1:5173// src/config/index.js
const path = require('node:path');
require('dotenv').config();
const { RAIZ } = require('./rutas.js');
// Lectores tipados: cada uno lanza si el valor no cumple lo esperado.
const opcional = (nombre, porDefecto) => (process.env[nombre] ?? '').trim() || porDefecto;
function obligatoria(nombre) {
const valor = opcional(nombre, '');
if (valor === '') throw new Error(`Falta la variable obligatoria: ${nombre}`);
return valor;
}
function entero(nombre, porDefecto, { minimo, maximo }) {
const numero = Number.parseInt(opcional(nombre, String(porDefecto)), 10);
if (!Number.isInteger(numero) || numero < minimo || numero > maximo) {
throw new Error(`La variable ${nombre} debe ser un entero entre ${minimo} y ${maximo}`);
}
return numero;
}
const lista = (n) => opcional(n, '').split(',').map((e) => e.trim()).filter(Boolean);
const ENTORNOS_VALIDOS = ['desarrollo', 'pruebas', 'produccion'];
function construirConfiguracion() {
const entorno = opcional('NODE_ENV', 'desarrollo');
if (!ENTORNOS_VALIDOS.includes(entorno)) {
throw new Error(`NODE_ENV debe ser uno de: ${ENTORNOS_VALIDOS.join(', ')}`);
}
return Object.freeze({
entorno,
esProduccion: entorno === 'produccion',
puerto: entero('PUERTO', 3000, { minimo: 1, maximo: 65535 }),
host: opcional('HOST', '127.0.0.1'),
limiteCuerpo: opcional('LIMITE_CUERPO', '100kb'),
confiarEnProxy: entero('CONFIAR_EN_PROXY', 0, { minimo: 0, maximo: 10 }),
origenesPermitidos: lista('ORIGENES_PERMITIDOS'),
directorioPublico: path.join(RAIZ, 'publico'),
// En produccion exigimos clave de firma; en desarrollo hay valor de relleno.
clavePasarela: entorno === 'produccion'
? obligatoria('CLAVE_PASARELA')
: opcional('CLAVE_PASARELA', 'clave-de-desarrollo'),
});
}
let configuracion;
try {
configuracion = construirConfiguracion();
} catch (error) {
console.error(`[configuracion] ${error.message}`); // fallar rapido
process.exit(1);
}
module.exports = { configuracion, ENTORNOS_VALIDOS };Las ideas que hay detrás de este fichero:
- Un único punto de lectura de
process.env. Buscarlo en el proyecto debe dar exactamente un resultado: este fichero. Así sabes de un vistazo qué necesita la aplicación para arrancar. - Fallar rápido. Si falta la clave de la pasarela en producción, el proceso muere en el arranque con un mensaje claro. La alternativa —arrancar y fallar tres horas después, en la primera venta— es mucho peor.
- Tipos correctos.
process.env.PUERTOes la cadena'3000'; la configuración expone el número3000y evita comparaciones sorpresa. - Objeto congelado con
Object.freeze: nadie cambia la configuración en tiempo de ejecución. - El
.envnunca va al repositorio. Se versiona.env.ejemplo; añade.enva.gitignore.
En el módulo 11 ampliaremos esto con secretos gestionados; por ahora, con esto basta.
- Montar la aplicación en un servidor
http propio
http propioEn 06-01 vimos que app es una función manejadora. Aquí lo aprovechamos: escribimos http.createServer(aplicacion) en lugar de aplicacion.listen(...). ¿Qué ganas exactamente?
| Ventaja | Para qué la necesitarás |
|---|---|
Tienes la referencia al Server |
Cerrarlo con servidor.close() en el apagado ordenado (apartado 8) |
| Puedes acoplar otros protocolos al mismo puerto | Socket.IO en el módulo 12 hace new Server(servidor) |
| Puedes ajustar tiempos de espera | servidor.keepAliveTimeout, servidor.headersTimeout en el módulo 11 |
| Puedes escuchar eventos del servidor | servidor.on('error', ...) para capturar EADDRINUSE con un mensaje decente |
| Puedes tener dos servidores con la misma app | HTTP y HTTPS, o un puerto de métricas aparte |
// El error clasico: el puerto ya esta ocupado.
servidor.on('error', (error) => {
if (error.code !== 'EADDRINUSE') throw error;
console.error(`[escena-viva] el puerto ${configuracion.puerto} ya esta en uso`);
process.exit(1);
});
- Apagado ordenado con
SIGTERM
SIGTERMCuando un orquestador (PM2, Docker, Kubernetes) quiere parar tu proceso, le envía la señal SIGTERM. Si no haces nada, Node muere de inmediato y las peticiones en curso se cortan a mitad: un comprador podría ver su navegador colgado justo después de enviar un pedido. El apagado ordenado consiste en dejar de aceptar conexiones nuevas, terminar las que están en curso, cerrar recursos y salir.
// src/servidor.js (continuacion)
const MILISEGUNDOS_DE_GRACIA = 10_000;
function registrarApagadoOrdenado(servidor) {
let apagando = false;
function apagar(senal) {
if (apagando) return; // una segunda senal no duplica el apagado
apagando = true;
console.error(`[escena-viva] recibida ${senal}, cerrando ordenadamente...`);
// 1. Dejamos de aceptar conexiones nuevas; las en curso terminan.
servidor.close((error) => {
if (error) console.error('[escena-viva] error al cerrar:', error.message);
process.exit(error ? 1 : 0);
});
// 2. Cerramos las conexiones keep-alive ociosas para no esperar a que expiren.
servidor.closeIdleConnections();
// 3. Red de seguridad; unref evita que el temporizador retenga el proceso.
setTimeout(() => {
console.error('[escena-viva] cierre forzado tras el tiempo de gracia');
process.exit(1);
}, MILISEGUNDOS_DE_GRACIA).unref();
}
process.on('SIGTERM', () => apagar('SIGTERM'));
process.on('SIGINT', () => apagar('SIGINT')); // Ctrl+C en desarrollo
}Detalles que la gente suele saltarse: la bandera apagando evita que pulsar Ctrl+C dos veces lance dos apagados simultáneos; sin closeIdleConnections() una conexión keep-alive ociosa mantiene el servidor vivo hasta que expire y el cierre se eterniza; y el temporizador con unref() garantiza que el proceso termine aunque algo se atasque, sin impedir que termine antes si todo va bien. Aquí es donde en el módulo 11 añadiremos el cierre de la base de datos y el volcado de los registros pendientes.
app.locals frente a res.locals
app.locals frente a res.localsDos almacenes con nombres parecidos y vidas muy distintas.
app.locals |
res.locals |
|
|---|---|---|
| Ámbito | Toda la aplicación | Una única petición/respuesta |
| Vida | Desde el arranque hasta el apagado | Desde que llega la petición hasta que se responde |
| Compartido entre usuarios | Sí | No |
| Uso típico | Nombre de la plataforma, versión, constantes | Identificador de petición, usuario autenticado, instante de inicio |
// Globales: se fijan una vez, en crearAplicacion().
aplicacion.locals.salas = ['Teatro Almendra', 'Sala Boveda', 'Auditorio Ribera'];
// Por peticion: los pone un middleware (06-04).
aplicacion.use((peticion, respuesta, siguiente) => {
respuesta.locals.idPeticion = crypto.randomUUID();
siguiente();
});Error grave y frecuente: guardar datos del usuario actual en
app.locals. Como es global, el segundo usuario vería los datos del primero. Todo lo que dependa de quién hace la petición va enres.locals(o enreq, como haremos conreq.eventoyreq.datosValidados).
- Una nota sobre motores de vistas
Express sabe renderizar plantillas en el servidor: app.set('view engine', 'ejs'), app.set('views', ...) y res.render('evento', { evento }). Es la forma clásica de generar HTML desde Node y sigue siendo válida para sitios con contenido servido.
Escena Viva no la usa. Nuestro front-end es la carpeta publico/ (HTML, CSS y un app.js que llama a la API con fetch), y el servidor es una API JSON: la misma API sirve a un front-end web, a una app móvil o a un panel interno, y sus respuestas se prueban con supertest sin parsear HTML. Lo que sí conservamos es express.static, que sirve esos ficheros y sustituye a tu estaticos.js del módulo 4; lo desmenuzaremos en 06-04.
Errores Comunes y Consejos
- Llamar a
listen()dentro deapp.js. Rompe las pruebas del módulo 9 y el apagado del módulo 11. Si ves unlistenfuera deservidor.js, es un error. - Exportar la app ya construida (
module.exports = app) en vez de una factoría. Funciona hasta que una prueba necesita otra configuración; entonces hay que refactorizarlo todo. process.envesparcido por veinte ficheros. Imposible saber qué necesita la aplicación. Centraliza enconfig/index.js.- Confundir
app.get('nombre')conapp.get('/ruta', manejador). Un argumento lee un ajuste; dos registran una ruta. - Activar
trust proxysin proxy. Regalas la capacidad de falsificar la IP a cualquiera. - Subir el
.enval repositorio. A.gitignoredesde el primer día; versiona.env.ejemplo. - Consejo: ejecuta
node -e "require('./src/config/index.js')"en el scriptcomprobar. Si la configuración es inválida, lo sabrás en la integración continua y no en producción.
Ejercicios
Ejercicio 1: la separación en la práctica
Crea src/app.js y src/servidor.js con la estructura de esta lección y demuestra que la separación funciona: escribe un tercer fichero probar-app.js que importe crearAplicacion(), la monte en un http.createServer sobre el puerto 0 (Node asigna uno libre), haga una petición a /salud con fetch y cierre el servidor. No debe tocar src/servidor.js en ningún momento.
Ejercicio 2: configuración que falla rápido
Amplía src/config/index.js con una variable MONEDA_POR_DEFECTO que solo admita EUR, USD o GBP, con EUR por defecto. Comprueba que un valor inválido impide el arranque con un mensaje claro y código de salida 1.
Ejercicio 3: apagado observable
Añade al apagado ordenado un contador de peticiones en curso: un middleware lo incrementa al entrar y lo decrementa en res.on('finish'). Al recibir SIGTERM, el servidor debe imprimir por stderr cuántas peticiones quedaban vivas. Pruébalo con un endpoint lento (/lento, que tarda 3 segundos) y enviando la señal mientras está en curso.
Soluciones
Solución 1
// probar-app.js
const http = require('node:http');
const { crearAplicacion } = require('./src/app.js');
async function principal() {
const servidor = http.createServer(crearAplicacion({ servirEstaticos: false }));
// Puerto 0: el sistema operativo asigna uno libre. Es lo que hace supertest.
await new Promise((resolver) => servidor.listen(0, '127.0.0.1', resolver));
const respuesta = await fetch(`http://127.0.0.1:${servidor.address().port}/salud`);
console.log(JSON.stringify({ estado: respuesta.status, cuerpo: await respuesta.json() }));
await new Promise((resolver) => servidor.close(resolver));
}
principal().catch((error) => {
console.error('[probar-app]', error.message);
process.exitCode = 1;
});Que esto sea posible sin arrancar src/servidor.js es exactamente la prueba de que la separación está bien hecha.
Solución 2
// Dentro de src/config/index.js
const MONEDAS_VALIDAS = ['EUR', 'USD', 'GBP'];
function moneda(nombre, porDefecto) {
const valor = opcional(nombre, porDefecto).toUpperCase();
if (!MONEDAS_VALIDAS.includes(valor)) {
throw new Error(`${nombre} debe ser una de: ${MONEDAS_VALIDAS.join(', ')}`);
}
return valor;
}
// ...dentro de construirConfiguracion(): monedaPorDefecto: moneda('MONEDA_POR_DEFECTO', 'EUR'),MONEDA_POR_DEFECTO=YEN node src/servidor.js
# [configuracion] MONEDA_POR_DEFECTO debe ser una de: EUR, USD, GBP (salida: 1)Solución 3
// src/servidor.js — fragmentos con el contador
let enCurso = 0;
// Registrado el PRIMERO para contar absolutamente todo.
aplicacion.use((peticion, respuesta, siguiente) => {
enCurso += 1;
respuesta.on('finish', () => { enCurso -= 1; });
siguiente();
});
aplicacion.get('/lento', (p, r) => setTimeout(() => r.json({ estado: 'ok' }), 3000));
process.on('SIGTERM', () => {
console.error(`[escena-viva] SIGTERM con ${enCurso} peticiones en curso`);
servidor.close(() => process.exit(0));
servidor.closeIdleConnections();
});Detalle importante: para que el contador cuente, hay que registrarlo antes que las rutas. Ese es el tema central de 06-04.
Conclusión
Ya no tienes un script de juguete, sino el esqueleto de una aplicación de verdad. La pieza central es la separación entre crearAplicacion() —que construye y devuelve la app sin arrancarla— y src/servidor.js —que crea el servidor http, lo pone a escuchar y sabe apagarlo ordenadamente cuando llega SIGTERM—. Esa separación no es estética: es lo que hará posible probar con supertest en el módulo 9, acoplar Socket.IO en el módulo 12 y desplegar con PM2 y Docker en el módulo 11. Además has definido la estructura de carpetas de Escena Viva con responsabilidades claras (y sabes cuándo no aplicarla), has elegido a conciencia los ajustes del framework (x-powered-by fuera, trust proxy según entorno, case sensitive routing activo) y has centralizado la configuración en un módulo que lee process.env una sola vez, valida tipos y rangos y falla rápido si algo no cuadra.
La app, sin embargo, tiene una única ruta: /salud. En la siguiente lección, Enrutamiento en Express, la llenamos: parámetros de ruta comparados con el compilarPatron que escribiste a mano, express.Router() para montar /api/eventos, /api/sesiones y /api/pedidos como módulos independientes, router.param() para cargar el evento una sola vez, y la regla que más quebraderos de cabeza evita: el orden de declaración de las rutas.
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
