El modelo de servidor HTTP de Node.js
Cada framework HTTP que utilizarás en Node (Express, Fastify, NestJS) es una capa de conveniencias envuelta alrededor de la misma primitiva: el módulo node:http.
Busca en todas las páginas de la documentación
Cada framework HTTP que utilizarás en Node (Express, Fastify, NestJS) es una capa de conveniencias envuelta alrededor de la misma primitiva: el módulo node:http.
Ese módulo no es un framework en sí mismo.
Es una traducción delgada y deliberadamente de bajo nivel de bytes TCP sin procesar a dos objetos JavaScript (una solicitud y una respuesta), y deja casi todo lo demás (enrutamiento, análisis, validación) para que otra cosa lo agregue.
Comprender esta capa es importante incluso si nunca escribes http.createServer a mano, porque el comportamiento que depurarás en un framework (una respuesta colgada, un error de orden de encabezados, un tiempo de espera de cliente lento) es casi siempre un síntoma de que esta capa se está filtrando.
Conceptos básicos de HTTP en Node te ofrece ejemplos prácticos de este módulo; esta página es el modelo subyacente a esos ejemplos: qué son realmente una "solicitud" y una "conexión", y cómo los frameworks se sitúan encima sin reemplazar nada de ello.
http de Node convierte los datos sin procesar del socket TCP en un IncomingMessage (la solicitud) y un ServerResponse (la respuesta) basados en flujos, y cada framework de esta categoría se ejecuta como la devolución de llamada conectada a ese mismo evento request de bajo nivel.IncomingMessage, ServerResponse, keep-alive.Una conexión y una solicitud no son lo mismo, y confundirlas es la fuente de confusión más común en esta capa.
Una conexión es un socket TCP: una tubería sin procesar y persistente entre un cliente y el servidor.
Una solicitud es un mensaje HTTP que viaja a través de ese socket.
Gracias a la función keep-alive de HTTP/1.1, un solo socket transporta rutinariamente muchas solicitudes, una tras otra, sin reabrir la conexión cada vez, razón por la cual los límites de conexión del lado del servidor y los límites por solicitud son perillas genuinamente diferentes.
Cuando un cliente abre una conexión y envía un mensaje HTTP bien formado, el analizador HTTP incorporado de Node (llhttp) convierte los bytes sin procesar en dos objetos y dispara un evento request en el servidor: un IncomingMessage y un ServerResponse.
import { createServer } from "node:http";
const server = createServer((req, res) => {
// req: IncomingMessage - un flujo legible del cuerpo de la solicitud
// res: ServerResponse - un flujo escribible a través del cual envías la respuesta
res.end("ok");
});Ambos objetos son flujos antes que cualquier otra cosa.
IncomingMessage es un flujo legible: el cuerpo llega en fragmentos, no como una sola cadena, porque Node no tiene idea de antemano de cuán grande será el cuerpo de la solicitud.
ServerResponse es un flujo escribible: llamar a res.write() envía bytes inmediatamente si los encabezados ya se han enviado, o los pone en cola hasta que lo hagan.
Este diseño centrado en el flujo es la razón por la que los frameworks que "analizan JSON automáticamente" en realidad solo están haciendo el trabajo de recopilación de fragmentos que se muestra en Conceptos básicos de HTTP en Node por ti, detrás de una llamada .json().
La ruta desde un paquete TCP de un cliente hasta tu función de manejador tiene una forma fija: el sistema operativo entrega una conexión aceptada a net.Server, http.Server adjunta su analizador a ese socket, el analizador decodifica incrementalmente los bytes en semántica HTTP (método, ruta, encabezados, fragmentos del cuerpo), y una vez que ha llegado una línea de solicitud y encabezados completos, Node dispara request con tu par IncomingMessage/ServerResponse.
Nada de esta ruta es exclusivo de Express o Fastify; ambos llaman literalmente a http.createServer(listener) (o un equivalente) bajo el capó, y pasan su propia función de listener como la devolución de llamada.
Socket TCP aceptado
│
▼
Analizador HTTP (llhttp) decodifica bytes incrementalmente
│
▼
El evento 'request' se dispara ──────────────► el listener del framework se ejecuta
│ (aplicación Express, enrutador Fastify, ...)
▼
el cuerpo llega como fragmentos de flujo con el tiempo, independientemente del evento anteriorLo que difiere entre los frameworks es enteramente lo que sucede dentro de esa función de listener: Express recorre una cadena de middleware, Fastify despacha a través de un enrutador compilado con ganchos de ciclo de vida, NestJS resuelve un gráfico de módulos antes de delegar al adaptador con el que esté configurado.
Ninguno de ellos obtiene un objeto de solicitud diferente del sistema operativo; todos decoran o envuelven el mismo par IncomingMessage/ServerResponse que Node les entregó.
Esa es una heurística de depuración útil: si algo falla idénticamente en cada framework que pruebas, es muy probable que el error esté en esta capa, no en la suya.
Los encabezados merecen una atención especial porque Node impone una regla de ordenación que confunde tanto al código del módulo sin procesar como al del framework: res.writeHead() envía el estado y los encabezados juntos, inmediatamente, mientras que res.setHeader() solo pone en cola un encabezado para que se envíe cuando los primeros bytes realmente salgan (ya sea a través de la primera write() o a través de end()).
Una vez que se han enviado los encabezados, por cualquiera de los métodos, cualquier intento posterior de establecerlos arroja ERR_HTTP_HEADERS_SENT, que es exactamente lo que sucede cuando un manejador responde dos veces después de que un framework ya envió una página de error en su nombre.
A escala, esta capa es donde residen los modos de falla a nivel de conexión, y no desaparecen solo porque hayas agregado un framework.
Un cliente lento o malicioso que abre una conexión y gotea bytes puede mantener un socket (y el manejador que lo espera) abierto mucho más tiempo de lo que lo haría una solicitud legítima; server.requestTimeout y server.headersTimeout existen específicamente para limitar eso, y cada implementación de framework en producción debe configurarlos explícitamente en lugar de confiar en los valores predeterminados (históricamente permisivos).
Los proxies inversos cambian lo que "el cliente" significa en esta capa: un equilibrador de carga o CDN termina la conexión real del cliente y abre su propia conexión a tu proceso de Node, por lo que req.socket.remoteAddress informa la IP del proxy a menos que confíes y analices deliberadamente X-Forwarded-For; consulta Conciencia del proxy inverso para obtener los detalles.
HTTP/2 y HTTP/3 cambian más de este modelo de lo que la mayoría de los desarrolladores esperan: HTTP/2 multiplexa muchas solicitudes lógicas sobre una única conexión TCP utilizando ID de flujo, lo que rompe la intuición simple de "un socket es aproximadamente igual al tráfico de un cliente" que esta página ha estado construyendo; consulta Consideraciones de HTTP/2 y HTTP/3.
| Enfoque | Fortaleza | Debilidad | Mejor ajuste |
|---|---|---|---|
node:http sin procesar | Sin sobrecarga de dependencias; control total sobre cada byte | Sin enrutamiento, análisis o validación: lo construyes todo | Sidecars de verificación de estado, herramientas internas pequeñas, aprendizaje del modelo |
Express sobre http | Enorme ecosistema; modelo mental simple sobre esta capa | La cadena de middleware plana tiene un techo de rendimiento real a alto rendimiento | La mayoría de las API CRUD, equipos que priorizan la familiaridad |
Fastify sobre http | Serialización compilada por esquema; estructura de complementos encapsulada | Más estructura inicial de la que una aplicación pequeña podría necesitar | API de alto rendimiento, equipos centrados en esquemas |
| NestJS (adaptador sobre Express/Fastify) | Arquitectura basada en DI a escala; transporte agnóstico al framework | La abstracción más pesada sobre esta capa; curva de aprendizaje más pronunciada | Equipos grandes, servicios empresariales de larga duración |
La observabilidad en esta capa es barata y a menudo se omite: los eventos request, close y clientError en http.Server te permiten medir la salud a nivel de conexión (solicitudes mal formadas, desconexiones abruptas) independientemente de lo que haga el propio registro de tu framework, lo cual es útil precisamente porque no está filtrado por la lógica de enrutamiento a nivel de framework.
keepAliveTimeout) y la configuración a nivel de solicitud (como requestTimeout) rigen cosas diferentes.http de Node." Lo envuelven: cada aplicación Express, Fastify o NestJS-sobre-Express sigue siendo una llamada a http.createServer con una función de listener más elaborada dentro.req.body en Express) es un framework que almacena en búfer y analiza esos fragmentos por ti primero.res.end() es opcional si ya has llamado a res.write()." El cliente sigue esperando hasta que se llama a end() (o la conexión expira); una respuesta no se "envía" hasta que se cierra explícitamente.write() o end()), los cambios posteriores en los encabezados arrojan ERR_HTTP_HEADERS_SENT.La capa de traducción incorporada de Node entre los bytes sin procesar del socket TCP y dos objetos JavaScript basados en flujos (un IncomingMessage (solicitud) y un ServerResponse (respuesta)) sobre los que cada framework HTTP construye sus propias abstracciones.
No. Express y Fastify llaman directamente a http.createServer() (o al equivalente HTTPS/HTTP2); NestJS delega al adaptador que configures, que a su vez es Express o Fastify. Todos reciben el mismo par IncomingMessage/ServerResponse de Node.
Node no tiene forma de saber el tamaño total de un cuerpo de antemano, por lo que lo entrega como un flujo legible de fragmentos a medida que llegan a través de la red. req.body solo existe porque un framework (o tu propio código) recopiló y analizó esos fragmentos primero.
El socket TCP permanece abierto después de que finaliza una respuesta, por lo que la siguiente solicitud del mismo cliente puede reutilizarlo en lugar de pagar el costo de un nuevo handshake TCP. server.keepAliveTimeout controla cuánto tiempo Node mantiene abierto ese socket inactivo esperando una próxima solicitud.
Casi siempre es un res.end() faltante o inalcanzable: un flujo de respuesta que nunca se cierra explícitamente deja al cliente (y la conexión) esperando indefinidamente, o hasta que interviene un tiempo de espera.
writeHead() envía el estado y los encabezados inmediatamente, como una sola acción. setHeader() solo pone en cola un valor de encabezado para que se incluya cuando la respuesta comience a vaciarse, ya sea la primera write() o la llamada a end(), lo que ocurra primero.
Porque un proxy inverso o un equilibrador de carga generalmente termina la conexión del cliente real y abre su propia conexión a tu proceso de Node, por lo que la dirección a nivel de socket es la del proxy, no la del cliente original, a menos que confíes y analices explícitamente un encabezado X-Forwarded-For.
Sí, para servicios pequeños y de un solo propósito: endpoints de verificación de estado, sidecars internos o cualquier cosa donde el enrutamiento y el análisis del cuerpo agreguen más sobrecarga que valor. La mayoría de las API de aplicaciones aún se benefician de la estructura de un framework una vez que crecen más allá de un puñado de rutas.
Multiplexa múltiples intercambios lógicos de solicitud/respuesta sobre una única conexión TCP utilizando ID de flujo, en lugar de que una solicitud termine antes de que comience la siguiente, lo que rompe la intuición simple de "una conexión se asigna aproximadamente a una solicitud a la vez" que esta página construye alrededor de HTTP/1.1.
Que el código en algún lugar intentó establecer un encabezado o un código de estado después de que la respuesta ya había comenzado a enviarse, generalmente una señal de que un manejador se ejecuta dos veces o de que se ejecuta una ruta de error de respaldo después de que una respuesta normal ya se completó.
No, todos leen el mismo flujo subyacente, pero difieren en los valores predeterminados (límites de tamaño, manejo de tipos de contenido) y en cuándo ocurre el análisis en el pipeline. Express requiere express.json() explícitamente; Fastify incluye el análisis JSON de forma predeterminada con límites conscientes del esquema.
Para limitar cuánto tiempo un cliente lento o con mal comportamiento puede mantener abierta una conexión (y los recursos del servidor detrás de ella), en lugar de depender de los valores predeterminados que históricamente permitían esperas casi ilimitadas, un vector real para el agotamiento de la conexión bajo carga.
createServer, encabezados y streamingVersiones de la pila: Esta página fue escrita para Node.js 24.18.0 (LTS activa), npm 10+ y TypeScript 5.6+.
Revisado por Chris St. John·Última actualización: 15 jul 2026