En el módulo 4 escribiste un servidor HTTP completo sin una sola dependencia: un enrutador con patrones compilados a expresiones regulares, un lector de cuerpo con límite de tamaño, un servidor de ficheros estáticos con ETag y compresión, y una tabla que traducía los códigos de error del dominio a estados HTTP. Fue mucho trabajo, y fue trabajo bien invertido: ahora sabes exactamente qué hace un framework web por dentro. En este módulo llega Express, y tu experiencia va a ser distinta de la del principiante que lo aprende de cero. Donde otros ven magia, tú vas a reconocer piezas que ya construiste.

Esta primera lección responde a una pregunta muy concreta: qué problema resuelve Express exactamente. No "por qué es popular" ni "cómo se usa en general", sino qué líneas de código desaparecen de tu proyecto cuando lo instalas, y qué líneas siguen siendo tuyas para siempre.

Contenido

  1. Qué es un framework web
  2. La comparación honesta: el mismo endpoint, dos veces
  3. Qué te da Express y qué sigue siendo tuyo
  4. La filosofía minimalista y sus consecuencias
  5. El ecosistema: alternativas reales
  6. Instalar Express con los ojos del módulo 5
  7. Express 5 frente a Express 4
  8. El primer app.js de Escena Viva
  9. Desmontando la magia: req y res son los de node:http
  10. Qué es app en realidad

  1. Qué es un framework web

Un framework web es una biblioteca que se coloca entre el módulo node:http y tu código de negocio, y que resuelve los problemas que aparecen siempre, en cualquier servidor HTTP, sea cual sea el dominio. Nadie escribe una API sin necesitar:

  • Decidir qué función atiende cada combinación de método y ruta (enrutamiento).
  • Extraer parámetros de la URL (/eventos/evt-001 → id = 'evt-001').
  • Ejecutar tareas comunes antes de cada manejador: registrar la petición, leer el cuerpo, comprobar cabeceras (middleware).
  • Serializar respuestas JSON con el Content-Type y la longitud correctos.
  • Servir ficheros estáticos.
  • Capturar errores en un punto único y convertirlos en respuestas coherentes.

Tú resolviste esos seis problemas a mano en el módulo 4. Un framework los resuelve por ti, y lo hace con un código que han revisado miles de personas durante más de una década.

La palabra clave es transversal: el framework no sabe nada de aforos, sesiones ni entradas. Eso es tuyo. El framework sabe de HTTP.

  1. La comparación honesta: el mismo endpoint, dos veces

Vamos a poner lado a lado el mismo endpoint, GET /eventos/:id, tal y como quedó en el módulo 4 y tal y como queda con Express. Sin trampas: el código artesanal es el que tú escribiste, y hace exactamente lo mismo.

Versión artesanal (módulo 4)

// Extracto de src/servidor/rutas-api.js del modulo 4 (resumido).
const { crearEnrutador } = require('./enrutador.js');
const { responderJson, responderError } = require('./respuestas.js');
const { obtenerEventoPorId } = require('../catalogo-datos.js');

const enrutador = crearEnrutador();

// 1. Registro de la ruta: el patron se compila a expresion regular.
enrutador.registrar('GET', '/eventos/:id', async (peticion, respuesta, parametros) => {
  try {
    // 2. El parametro llega en un objeto que construyo mi propio emparejador.
    const evento = await obtenerEventoPorId(parametros.id);
    // 3. La serializacion, el Content-Type y el Content-Length son mios.
    responderJson(respuesta, 200, evento.toJSON());
  } catch (error) {
    // 4. La traduccion de codigo de dominio a estado HTTP es mia, y ademas
    //    tengo que acordarme de envolver CADA manejador asincrono en try/catch.
    responderError(respuesta, error);
  }
});

module.exports = { enrutador };

Y eso no cuenta las piezas de las que depende: enrutador.js (compilar patrones, emparejar, gestionar el 405 con Allow), respuestas.js (cuatro funciones de escritura) y errores-http.js (la tabla de estados). Entre las tres suman varios cientos de líneas que tuviste que escribir, entender y mantener.

Versión con Express

// src/rutas/eventos.js con Express 5.
const express = require('express');
const { obtenerEventoPorId } = require('../catalogo-datos.js');

const rutasEventos = express.Router();

rutasEventos.get('/:id', async (peticion, respuesta) => {
  const evento = await obtenerEventoPorId(peticion.params.id);
  respuesta.json(evento.toJSON());
});

module.exports = { rutasEventos };

Cuatro líneas de lógica frente a unas quince, y sobre todo cero líneas de infraestructura. Fíjate en qué ha desaparecido concretamente:

Pieza del módulo 4 Qué pasa con Express
compilarPatron / emparejar de enrutador.js Lo hace router.get('/:id') internamente
Construir el objeto parametros Llega como peticion.params
responderJson (cabeceras + JSON.stringify) Es respuesta.json()
El try/catch de cada manejador async En Express 5 el rechazo va solo al manejador de errores
El 405 con cabecera Allow Express lo gestiona al recorrer las rutas
normalizarRuta (barras finales, mayúsculas) Ajustes strict routing y case sensitive routing

Lo importante no es que sean menos líneas. Es que las líneas que quedan hablan solo de Escena Viva: buscar un evento y devolverlo. Todo lo demás era ruido necesario que ahora alguien mantiene por ti.

  1. Qué te da Express y qué sigue siendo tuyo

Aquí es donde muchos cursos mienten por omisión. Express resuelve un conjunto acotado de problemas, y hay una lista igual de larga de cosas que siguen siendo responsabilidad tuya.

Área ¿Lo resuelve Express? Comentario
Enrutamiento y parámetros Sí Núcleo del framework
Cadena de middleware Sí Núcleo del framework
Respuestas (json, send, status, sendFile) Sí Sustituye a tu respuestas.js
Ficheros estáticos con ETag y caché Sí (express.static) Sustituye a estaticos.js + tipos-mime.js
Lectura del cuerpo con límite Sí (express.json({ limit })) Sustituye a cuerpo.js, 413 incluido
Punto único de manejo de errores Sí (middleware de 4 argumentos) Pero la política de errores la defines tú
Validación de datos de entrada No Lección 06-06, con zod
Seguridad (cabeceras, CORS, límites) No Middleware de terceros, lección 06-05
Autenticación y autorización No Módulo 8 completo
Acceso a datos / base de datos No Módulo 7
Arquitectura de carpetas y capas No Lección 06-02: decides tú
Registro estructurado y observabilidad No Módulo 11
Pruebas No Módulo 9, con supertest

Léelo dos veces. Express te ahorra la fontanería de HTTP. No te ahorra pensar. Un servidor Express mal diseñado es exactamente igual de malo que un servidor node:http mal diseñado, solo que más corto.

  1. La filosofía minimalista y sus consecuencias

Express se describe a sí mismo como fast, unopinionated, minimalist. La palabra que más importa es unopinionated: sin opiniones. Express no te dice dónde poner las carpetas, cómo separar controladores de servicios, qué validador usar ni cómo estructurar los errores. Solo te da el mecanismo.

Eso tiene dos caras:

Ventajas

  • Puedes adaptar la estructura a tu proyecto real, no al ejemplo de la documentación.
  • La superficie de API que hay que aprender es pequeña: enrutamiento, middleware, req, res, y poco más.
  • Se integra con cualquier cosa, porque no impone nada.

Inconvenientes

  • Dos proyectos Express pueden no parecerse en nada, lo que dificulta entrar en un código ajeno.
  • Tienes que tomar decisiones que un framework con opiniones tomaría por ti: validación, errores, capas, configuración.
  • Es fácil acabar con un app.js de 800 líneas si nadie impone disciplina.

Este módulo va precisamente de eso: tomar esas decisiones bien y una sola vez, y dejarlas escritas en la estructura del proyecto de Escena Viva.

  1. El ecosistema: alternativas reales

Express no es la única opción, y conviene saber qué hay al lado para no defender una herramienta por costumbre.

Framework Idea central Fuerte en A cambio
Express Middleware minimalista Ecosistema enorme, documentación y respuestas por todas partes, curva suave Poco opinado; validación y esquemas los pones tú
Fastify Rendimiento y esquemas JSON Muy rápido, validación y serialización por esquema integradas, plugins encapsulados Ecosistema menor, conceptos propios (encapsulación, hooks)
Koa Middleware con async/await y contexto ctx Núcleo minúsculo y elegante, control fino del flujo Casi todo es un paquete aparte; menos material didáctico
NestJS Arquitectura con opiniones (decoradores, inyección de dependencias) Proyectos grandes y equipos numerosos, estructura impuesta Mucho que aprender antes de la primera ruta; TypeScript de facto
Hapi Configuración declarativa Validación y seguridad integradas, todo por configuración Estilo muy propio, comunidad más pequeña

Por qué este curso enseña Express: porque los conceptos que aprendes aquí (middleware en cadena, enrutamiento, manejo centralizado de errores) son los mismos que usan Koa, Fastify y NestJS con otra sintaxis, y porque es el framework que más te vas a encontrar en código existente. Aprender Express es aprender el vocabulario del ecosistema.

  1. Instalar Express con los ojos del módulo 5

Vamos a instalarlo, pero no con los ojos cerrados. En el módulo 5 aprendiste a auditar lo que entra en node_modules; aplícalo ahora.

# Instalacion como dependencia de produccion.
npm install express

# ¿Que version ha entrado exactamente?
npm ls express

# ¿Cuantos paquetes hay ahora en total en el arbol?
npm ls --all --parseable | wc -l

# Licencias y metadatos del paquete.
npm view express version license repository.url

# Auditoria de seguridad tras la instalacion.
npm audit

Qué deberías observar y cómo interpretarlo:

  • Express no es una única pieza: arrastra varias decenas de dependencias transitivas (router, body-parser, send, serve-static, cookie, mime-types, finalhandler, qs…). No te asustes: la mayoría son módulos diminutos del mismo equipo, muchos de ellos son literalmente las piezas que tú escribiste a mano (send es tu estaticos.js, mime-types es tu tipos-mime.js, body-parser es tu cuerpo.js).
  • Licencia MIT, permisiva, sin obligaciones para tu proyecto.
  • Express es un proyecto de la OpenJS Foundation, con gobernanza pública. Eso es exactamente el tipo de señal que aprendiste a buscar: no depende de una sola persona.
  • Fija la versión en package.json y respeta el package-lock.json: en despliegue se instala con npm ci, no con npm install.

Tras la instalación, tu package.json incluirá algo como:

{
  "dependencies": {
    "dotenv": "^17.0.0",
    "express": "^5.1.0"
  }
}

  1. Express 5 frente a Express 4

Express 5 es la versión estable actual, y hay mucho código y muchos tutoriales escritos para la 4. Estas son las diferencias que de verdad te van a afectar:

Aspecto Express 4 Express 5
Manejadores async que rechazan Se quedaban colgados salvo que envolvieras el manejador El rechazo pasa automáticamente al manejador de errores
req.query Objeto mutable, se podía reasignar Getter de solo lectura: req.query = ... lanza error
Comodines /archivos/* con req.params[0] /archivos/*ruta con req.params.ruta (nombre obligatorio)
Parámetros opcionales /eventos/:id? /eventos{/:id} (sintaxis de grupo opcional)
body-parser Paquete aparte para algunos casos express.json() y express.urlencoded() integrados
res.status(...) con código inválido Se aceptaba Lanza error si no es un entero de 100 a 999

Las dos primeras filas son las que más código antiguo rompen. La primera es una mejora enorme que veremos a fondo en 06-07; la segunda causa un error muy concreto cuando alguien intenta "limpiar" la query reasignándola, y lo trataremos en 06-06.

  1. El primer app.js de Escena Viva

Diez líneas útiles. Créalo como app-minimo.js en la raíz solo para esta prueba; en 06-02 lo estructuraremos de verdad.

// app-minimo.js
const express = require('express');

const aplicacion = express();

// Una ruta de salud, igual que la GET /salud del modulo 4.
aplicacion.get('/salud', (peticion, respuesta) => {
  respuesta.json({ estado: 'ok', instante: new Date().toISOString() });
});

// Un endpoint con parametro de ruta.
aplicacion.get('/eventos/:id', (peticion, respuesta) => {
  respuesta.json({ id: peticion.params.id, sala: 'Teatro Almendra' });
});

aplicacion.listen(3000, () => {
  // Diagnosticos por stderr, como marca la convencion del curso.
  console.error('Escena Viva escuchando en http://localhost:3000');
});
node app-minimo.js
curl -s http://localhost:3000/salud
curl -s http://localhost:3000/eventos/evt-001
curl -si http://localhost:3000/ruta-que-no-existe | head -n 1

Repasa el resultado con atención:

  • GET /salud devuelve JSON con Content-Type: application/json; charset=utf-8 sin que tú hayas puesto la cabecera. Eso lo hace respuesta.json().
  • GET /eventos/evt-001 te devuelve el parámetro extraído. Eso lo hacía tu compilarPatron.
  • Una ruta inexistente devuelve 404 Not Found con un cuerpo HTML por defecto. En 06-07 lo sustituiremos por nuestro formato { error: { codigo, mensaje, estado } }.

  1. Desmontando la magia: req y res son los de node:http

Esta es la idea más importante de la lección. Los objetos req y res de Express no son objetos nuevos: son exactamente el IncomingMessage y el ServerResponse de node:http que ya conoces del módulo 4, con métodos añadidos a su prototipo. Vamos a demostrarlo.

// app-demostracion.js
const http = require('node:http');
const express = require('express');

const aplicacion = express();

aplicacion.get('/prueba', (peticion, respuesta) => {
  // 1. Son instancias de las clases del nucleo de Node.
  const esPeticionDelNucleo = peticion instanceof http.IncomingMessage;
  const esRespuestaDelNucleo = respuesta instanceof http.ServerResponse;

  // 2. Los metodos de node:http siguen ahi y funcionan.
  respuesta.setHeader('X-Escena-Viva', 'Teatro Almendra');
  respuesta.statusCode = 200;

  // 3. Y la peticion sigue siendo un stream legible del modulo 3.
  const esStream = typeof peticion.on === 'function' && typeof peticion.pipe === 'function';

  // 4. Metodo de Express, que por dentro usa los tres de arriba.
  respuesta.json({ esPeticionDelNucleo, esRespuestaDelNucleo, esStream });
});

aplicacion.listen(3000);
curl -si http://localhost:3000/prueba

Respuesta:

HTTP/1.1 200 OK
X-Escena-Viva: Teatro Almendra
Content-Type: application/json; charset=utf-8

{"esPeticionDelNucleo":true,"esRespuestaDelNucleo":true,"esStream":true}

Consecuencias prácticas de esto, que te van a servir durante todo el módulo:

  • Todo lo que aprendiste en el módulo 4 sobre res.writeHead, res.end, req.headers o req.method sigue siendo válido dentro de Express.
  • respuesta.json(objeto) es aproximadamente: fijar Content-Type, serializar con JSON.stringify y llamar a end(). Es tu responderJson, con más casos límite cubiertos (ETag, json spaces, caracteres especiales).
  • Como req sigue siendo un stream, puedes leerlo con pipeline para subidas grandes en lugar de acumularlo en memoria.
  • Si algo de Express no hace lo que quieres, siempre puedes bajar un piso al API de node:http. No hay una caja negra; hay una capa fina.

  1. Qué es app en realidad

Segunda desmitificación. express() devuelve una función. Concretamente, una función con la firma (req, res, next) a la que además se le han colgado propiedades y métodos (get, post, use, set, listen…). Y esa firma te resultará familiar: es la de un manejador de http.createServer.

const http = require('node:http');
const express = require('express');

const aplicacion = express();
aplicacion.get('/salud', (peticion, respuesta) => respuesta.json({ estado: 'ok' }));

// La prueba: app es una funcion.
console.error('Tipo de app:', typeof aplicacion); // 'function'

// Y por eso se le puede pasar directamente a createServer.
const servidor = http.createServer(aplicacion);
servidor.listen(3000, () => console.error('Servidor propio con app de Express'));

aplicacion.listen(3000) es solo un atajo que hace internamente http.createServer(this).listen(3000). Saber esto no es trivia: en 06-02 crearemos el servidor a mano precisamente para poder cerrarlo de forma ordenada, y en el módulo 12 lo necesitaremos para acoplar Socket.IO al mismo servidor.

Dicho de otro modo: Express es una función manejadora muy sofisticada. Toda la magia del framework consiste en que, cuando esa función se ejecuta, recorre una lista de middleware y rutas registradas y va llamando a las que encajan. Nada más. En 06-04 lo veremos con un diagrama.

Errores Comunes y Consejos

  • Creer que Express "es" el backend. Express es una capa de transporte HTTP. Tu lógica de negocio (las clases Evento, Sesion, GestorDeVentas) no debe saber que Express existe. Si un día cambias a Fastify, el dominio no se toca.
  • Seguir tutoriales de Express 4 con Express 5 instalado. Los síntomas típicos: rutas con :id? que dejan de funcionar, * sin nombre que lanza error al arrancar, y código que reasigna req.query y falla. Comprueba siempre npm ls express.
  • Instalar sin mirar. Aplica el criterio del módulo 5 a cada paquete, también a los del módulo 06-05. Express es fiable; no todos los middleware de terceros lo son.
  • Envolver todo en try/catch por costumbre. En Express 5 no hace falta para el caso general; solo cuando quieras convertir el error en otro más específico antes de propagarlo.
  • Olvidar que app es una función. Cuando algo raro pase con el arranque, recuerda que puedes construir el servidor tú y depurar en el nivel de node:http.
  • Consejo: conserva el código del módulo 4 en el repositorio. Durante todo este módulo vamos a compararlo con el nuevo, y esa comparación es la que convierte Express en algo que entiendes, no en algo que copias.

Ejercicios

Ejercicio 1: auditoría de la instalación

Instala Express en el proyecto de Escena Viva y responde por escrito: qué versión exacta entró, qué licencia tiene, cuántos paquetes hay en el árbol de dependencias antes y después, y cuáles de esas dependencias transitivas se corresponden con ficheros que escribiste a mano en el módulo 4.

Ejercicio 2: la ruta de salud, dos veces

Escribe dos ficheros que expongan el mismo endpoint GET /salud devolviendo { estado: 'ok', sala: 'Sala Boveda' }: uno con node:http puro (sin usar tu enrutador del módulo 4, todo en el manejador) y otro con Express. Cuenta las líneas de cada uno y anota qué tuviste que hacer a mano en el primero.

Ejercicio 3: demostrar el parentesco

Crea un endpoint GET /diagnostico en Express que devuelva un JSON con: el nombre de la clase de req (peticion.constructor.name), el nombre de la clase de res, la lista de cabeceras que ya ha fijado la respuesta antes de enviar, y el valor de peticion.httpVersion. Añade además una cabecera personalizada usando setHeader para comprobar que el API de node:http sigue disponible.

Soluciones

Solución 1

npm install express
npm ls express                       # p. ej. [email protected]
npm view express license             # MIT
npm ls --all --parseable | wc -l     # comparar antes y despues

Correspondencias con tu código del módulo 4:

Dependencia transitiva Fichero tuyo del módulo 4
body-parser src/servidor/cuerpo.js
serve-static y send src/servidor/estaticos.js
mime-types src/servidor/tipos-mime.js
router / path-to-regexp src/servidor/enrutador.js
finalhandler El 404/500 final de manejarPeticion

Solución 2

// salud-http.js — con node:http puro.
const http = require('node:http');

const servidor = http.createServer((peticion, respuesta) => {
  const url = new URL(peticion.url, `http://${peticion.headers.host}`);
  if (peticion.method !== 'GET') {
    respuesta.writeHead(405, { Allow: 'GET', 'Content-Type': 'application/json' });
    respuesta.end(JSON.stringify({ error: { codigo: 'METODO_NO_PERMITIDO', estado: 405 } }));
    return;
  }
  if (url.pathname !== '/salud') {
    respuesta.writeHead(404, { 'Content-Type': 'application/json' });
    respuesta.end(JSON.stringify({ error: { codigo: 'RUTA_NO_ENCONTRADA', estado: 404 } }));
    return;
  }
  const cuerpo = JSON.stringify({ estado: 'ok', sala: 'Sala Boveda' });
  respuesta.writeHead(200, {
    'Content-Type': 'application/json; charset=utf-8',
    'Content-Length': Buffer.byteLength(cuerpo),
  });
  respuesta.end(cuerpo);
});

servidor.listen(3000);
// salud-express.js — con Express.
const express = require('express');

const aplicacion = express();
aplicacion.get('/salud', (peticion, respuesta) => {
  respuesta.json({ estado: 'ok', sala: 'Sala Boveda' });
});
aplicacion.listen(3000);

A mano tuviste que: parsear la URL, comprobar el método, generar el 405 con Allow, generar el 404, serializar, calcular Content-Length y escribir las cabeceras. Express hace las siete cosas.

Solución 3

// diagnostico.js
const express = require('express');

const aplicacion = express();

aplicacion.get('/diagnostico', (peticion, respuesta) => {
  respuesta.setHeader('X-Escena-Viva-Sala', 'Auditorio Ribera');
  respuesta.json({
    clasePeticion: peticion.constructor.name,       // IncomingMessage
    claseRespuesta: respuesta.constructor.name,     // ServerResponse
    cabecerasYaFijadas: respuesta.getHeaderNames(), // incluye x-escena-viva-sala
    versionHttp: peticion.httpVersion,              // '1.1'
  });
});

aplicacion.listen(3000, () => console.error('Diagnostico en :3000'));

Lo relevante de la respuesta: los nombres de clase son los del núcleo de Node, no clases propias de Express, y getHeaderNames() es un método de ServerResponse que sigue estando disponible.

Conclusión

Express no es magia: es la abstracción de las seis tareas transversales de todo servidor HTTP —enrutamiento, parámetros, middleware, respuestas, estáticos y errores—, resueltas por ti a mano en el módulo 4 y ahora delegadas a un código maduro y auditado. Has visto la comparación línea a línea, la lista honesta de lo que Express no hace por ti (validación, seguridad, arquitectura, datos, pruebas), las alternativas del ecosistema, y las dos ideas que desmontan la aparente magia: req y res son los objetos de node:http con métodos añadidos, y app es simplemente una función manejadora que se le puede pasar a http.createServer.

También sabes ya en qué se diferencia Express 5 de Express 4 y por qué esas diferencias te van a ahorrar código en 06-07.

Lo que tienes ahora mismo es un app.js de juguete: diez líneas en la raíz del proyecto. Eso no es una aplicación mantenible. En la siguiente lección, Configuración de una Aplicación Express, daremos el salto: separaremos crearAplicacion() de la puesta en marcha del servidor —una separación que resulta obligatoria para poder probar con supertest en el módulo 9 y para el apagado ordenado del módulo 11—, definiremos la estructura de carpetas de Escena Viva, configuraremos los ajustes de la aplicación (trust proxy, x-powered-by, strict routing) y centralizaremos la configuración por entorno en un módulo que falle rápido si falta algo.

Curso de Node.js: De Principiante a Avanzado

Módulo 1: Introducción a Node.js

Módulo 2: Conceptos Básicos

Módulo 3: Sistema de Archivos y E/S

Módulo 4: HTTP y Servidores Web

Módulo 5: NPM y Gestión de Paquetes

Módulo 6: Framework Express.js

Módulo 7: Bases de Datos y ORMs

Módulo 8: Autenticación y Autorización

Módulo 9: Pruebas y Depuración

Módulo 10: Temas Avanzados

Módulo 11: Despliegue y DevOps

Módulo 12: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados