Creando servidores HTTP
Tres formas de crear servidores HTTP en Node.js — node:http sin dependencias, Express por familiaridad, Fastify por rendimiento.
Receta
Tarjeta de receta de referencia rápida — lista para copiar y pegar.
// 1. node:http — sin dependencias
import { createServer } from "node:http";
createServer((req, res) => {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true }));
}).listen(3000);
// 2. Express — la opción familiar
import express from "express";
const app = express();
app.use(express.json());
app.get("/", (req, res) => res.json({ ok: true }));
app.listen(3000);
// 3. Fastify — alto rendimiento
import Fastify from "fastify";
const fastify = Fastify({ logger: true });
fastify.get("/", async () => ({ ok: true }));
await fastify.listen({ port: 3000 });Cuándo usarlo: En cualquier momento que necesites un servicio HTTP de larga duración — APIs internas, webhooks, fallbacks SSR, workers en background con endpoint de salud.
Ejemplo funcional
Una pequeña API REST con tres rutas — GET /, GET /users/:id, POST /users — lado a lado en los tres frameworks.
// --- node:http ---
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
type User = { id: string; name: string };
const users = new Map<string, User>();
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
res.setHeader("content-type", "application/json");
if (req.method === "GET" && req.url === "/") {
res.end(JSON.stringify({ ok: true }));
return;
}
const idMatch = req.url?.match(/^\/users\/([^/]+)$/);
if (req.method === "GET" && idMatch) {
const user = users.get(idMatch[1]);
if (!user) {
res.statusCode = 404;
res.end(JSON.stringify({ error: "not found" }));
return;
}
res.end(JSON.stringify(user));
return;
}
if (req.method === "POST" && req.url === "/users") {
let body = "";
for await (const chunk of req) body += chunk;
const parsed = JSON.parse(body) as User;
users.set(parsed.id, parsed);
res.statusCode = 201;
res.end(JSON.stringify(parsed));
return;
}
res.statusCode = 404;
res.end(JSON.stringify({ error: "not found" }));
});
server.listen(3000, () => console.log("node:http en :3000"));// --- Express ---
import express, { type Request, type Response } from "express";
type User = { id: string; name: string };
const users = new Map<string, User>();
const app = express();
app.use(express.json());
app.get("/", (_req: Request, res: Response) => {
res.json({ ok: true });
});
app.get("/users/:id", (req: Request<{ id: string }>, res: Response) => {
const user = users.get(req.params.id);
if (!user) return res.status(404).json({ error: "not found" });
res.json(user);
});
app.post("/users", (req: Request<unknown, unknown, User>, res: Response) => {
users.set(req.body.id, req.body);
res.status(201).json(req.body);
});
app.listen(3000, () => console.log("express en :3000"));// --- Fastify ---
import Fastify from "fastify";
type User = { id: string; name: string };
const users = new Map<string, User>();
const fastify = Fastify({ logger: true });
fastify.get("/", async () => ({ ok: true }));
fastify.get<{ Params: { id: string } }>("/users/:id", async (req, reply) => {
const user = users.get(req.params.id);
if (!user) return reply.code(404).send({ error: "not found" });
return user;
});
fastify.post<{ Body: User }>(
"/users",
{
schema: {
body: {
type: "object",
required: ["id", "name"],
properties: {
id: { type: "string" },
name: { type: "string" },
},
},
},
},
async (req, reply) => {
users.set(req.body.id, req.body);
return reply.code(201).send(req.body);
}
);
await fastify.listen({ port: 3000 });Lo que esto demuestra:
node:httpte obliga a manejar enrutamiento, análisis del cuerpo y códigos de estado por ti mismo — mínimo pero verboso.- El middleware Express (
express.json()) te dareq.bodyde forma gratuita y utiliza semántica familiarreq/res. - Fastify añade validación basada en esquemas y serialización, más un logger incorporado, con manejadores async por defecto.
Análisis profundo
Cómo funciona
Los tres frameworks envuelven node:http. createServer devuelve un http.Server — en sí mismo un EventEmitter — que emite 'request' para cada mensaje entrante. Express y Fastify instalan un único listener 'request' que ejecuta sus pipelines de enrutamiento y middleware.
Express usa una cadena de middleware lineal: cada función llama a next() para pasar el control. Fastify usa un sistema de hooks impulsado por esquemas con serializadores JSON precompilados, que es de donde proviene la mayor parte de su velocidad.
Variaciones
node:http con análisis JSON. No hay un parser de cuerpo incorporado — lee la solicitud como un iterable async de buffers y analiza manualmente (ver el ejemplo funcional). Para cuerpos grandes, protege contra memoria no limitada con un límite de bytes.
Cadena de middleware Express. El orden importa. Pon express.json() antes de las rutas que necesiten req.body, pon manejadores de error (funciones con cuatro parámetros) último, y monta CORS/helmet antes de cualquier lógica empresarial.
Esquemas Fastify. Declarar un esquema JSON para body, params, querystring y response valida automáticamente la entrada y compila un serializador rápido para la respuesta. Obtienes errores 400 de forma gratuita en entrada incorrecta.
Manejadores de ruta async. Express 4 traga silenciosamente errores lanzados en manejadores async a menos que los envuelvas o uses express-async-errors. Express 5 reenvía promises rechazadas al middleware de error. Fastify espera manejadores async de forma nativa.
Apagado elegante. Escucha SIGTERM, detén de aceptar nuevas conexiones con server.close(), y drena solicitudes en vuelo antes de salir:
process.on("SIGTERM", () => {
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 10_000).unref();
});Endpoint de verificación de salud. Añade GET /healthz devolviendo 200 para liveness. Mantenlo libre de dependencias para que una DB rota no falle la sonda liveness — usa /readyz para verificaciones de dependencias.
Middleware CORS y error. Para Express, app.use(cors()) temprano, y un manejador de error final (err, req, res, next) => ... último. Para Fastify, registra @fastify/cors y usa setErrorHandler.
Notas de TypeScript
Para node:http, importa IncomingMessage y ServerResponse de node:http y anota la firma del manejador. Para Express, instala @types/express y usa Request y Response — Request toma genéricos para params, cuerpo de respuesta, cuerpo de solicitud y query: Request<Params, ResBody, ReqBody, Query>. Para Fastify, pasa un objeto genéricos directamente a la ruta: fastify.get<\{ Params: \{ id: string \} \}>("/users/:id", handler) (caracteres de escape entre llaves al escribir esto en markdown). Los genéricos de Fastify también admiten Body, Querystring, Headers y Reply.
Trampas
- Olvidar
res.end()en node:http. La solicitud se cuelga hasta que el cliente agota el tiempo. Cada ruta de código debe llamar ares.end()(ores.writeHead().end()), incluyendo ramas de error. - Errores no manejados en manejadores Express 4 async. Un error lanzado o una promise rechazada se traga silenciosamente y la solicitud se cuelga. Envuelve manejadores o actualiza a Express 5.
- Orden del middleware.
express.json()después de una ruta significa quereq.bodyesundefined. CORS después de rutas significa que las solicitudes preflight devuelven 404. Manejadores de error antes de rutas nunca se ejecutan. - Preflight CORS. Los navegadores envían
OPTIONSantes de solicitudes no simples. Si tu router solo registraGET/POST, preflight falla. Usa un middleware CORS que manejeOPTIONSglobalmente. EADDRINUSEal reiniciar. El proceso anterior aún mantiene el puerto. Mátalo, espera la ventana TIME_WAIT, o estableceserver.listen(\{ port, exclusive: false \})— llaves escapadas en prosa.- Análisis manual JSON en node:http. Leer todo el cuerpo en una cadena permite que un cliente malicioso OOM tu proceso. Refuerza un tamaño máximo y rechaza solicitudes demasiado grandes con 413.
- Mezclar
returnyres.senden Express. Devolver un valor no hace nada; Express solo responde cuando llamas ares.send/res.json. En Fastify, lo opuesto — devuelve el valor y se envía automáticamente.
Alternativas
| Opción | Mejor para | Notas |
|---|---|---|
node:http | Servicios mínimos, aprendizaje | Cero deps, control máximo, máximo boilerplate. |
| Express | Apps heredadas, ecosistema enorme | Familiar, lento en relación a Fastify, middleware-first. |
| Fastify | APIs JSON de alto rendimiento | Validación de esquema, serializador rápido, excelente soporte TS. |
| Hono | Runtimes de edge (Workers, Deno, Bun) | Pequeño, basado en fetch, también se ejecuta en Node. |
| Koa | Secuela de los creadores de Express | Middleware async, núcleo más pequeño, ecosistema más pequeño. |
| h3 | Motor de servidor Nitro / Nuxt | Componible, funciona en Node y edge. |
| Next.js API routes | Apps ya en Next.js | Co-ubicado con el frontend, menos flexible para trabajo no HTTP. |
Bun.serve | Runtime de Bun | Basado en fetch, extremadamente rápido, solo Bun. |
FAQs
¿Cuál de los tres debería elegir para una nueva API?
Fastify es la opción segura predeterminada para APIs JSON — rápido, validado por esquema, excelente soporte TypeScript. Elige Express solo si tu equipo ya lo conoce o necesitas un middleware específico. Elige node:http solo para pequeñas herramientas internas donde una dependencia es un problema.
¿Cómo analizo un cuerpo JSON sin un framework?
Itera la solicitud como un iterable async de buffers, concatena a una cadena, luego JSON.parse. Siempre refuerza un tamaño máximo — rastrea bytes leídos y rechaza con 413 si excede tu límite, o un cliente malicioso puede OOM el proceso.
¿Cuál es la relación entre http.Server y EventEmitter?
http.Server extiende EventEmitter y emite 'request', 'connection', 'close' y 'error'. Express y Fastify instalan un único listener 'request' que ejecuta sus propios pipelines.
¿Cómo implemento un apagado elegante?
Escucha SIGTERM, llama a server.close() para dejar de aceptar nuevas conexiones, deja que las solicitudes en vuelo terminen, luego sal. Añade un timeout (p. ej. 10s) que sale por la fuerza si algo se cuelga, y llama a .unref() en el temporizador para que no mantenga el loop vivo por sí solo.
¿Por qué recibo EADDRINUSE cuando reinicio mi servidor dev? (Trampa)
El proceso anterior aún mantiene el puerto — ya sea que no se apagó limpiamente, o el SO aún está en la ventana TIME_WAIT. Mata el proceso errante (lsof -i :3000) o cambia el puerto. Un manejador de apagado elegante previene esto en reinicios intencionales.
¿Por qué mi manejador Express async se cuelga en lugar de devolver un 500? (Trampa)
En Express 4, los errores lanzados y las promises rechazadas de manejadores async se tragan silenciosamente. Envuelve manejadores en try/catch, instala express-async-errors, o actualiza a Express 5 que reenvía rechazos al middleware de error automáticamente.
¿Cómo escribo parámetros de ruta Express? (TypeScript)
Request toma cuatro genéricos: Request<Params, ResBody, ReqBody, Query>. Para GET /users/:id, usa Request<\{ id: string \}>. Para un POST con un cuerpo tipado, usa Request<unknown, unknown, MyBody>. Instala @types/express primero.
¿Cómo escribo parámetros de ruta y cuerpo de Fastify? (TypeScript)
Pasa un objeto genéricos a la ruta: fastify.get<\{ Params: \{ id: string \}; Querystring: \{ q: string \} \}>(...). Las claves admitidas son Params, Querystring, Body, Headers y Reply. Fastify infiere req.params, req.query y req.body de estos tipos.
¿Necesito una librería CORS, o puedo establecer headers manualmente?
Puedes establecer Access-Control-Allow-Origin y similares a mano, pero también debes manejar solicitudes preflight OPTIONS. Una librería (cors para Express, @fastify/cors para Fastify) maneja ambas en una línea y evita bugs sutiles.
¿Por qué mi middleware se ejecuta después del manejador de ruta?
Express ejecuta middleware en orden de registro. El middleware registrado después de app.get(...) solo se ejecuta para rutas registradas después. Siempre monta parsers, CORS y loggers antes de tus rutas.
¿Cuál es la diferencia entre manejo de respuesta Express y Fastify?
Express solo responde cuando llamas a res.send / res.json — devolver un valor no hace nada. Fastify es lo opuesto: devuelve el valor y se envía automáticamente. Mezclar los dos modelos mentales es una fuente común de solicitudes colgadas.
¿Debería usar Next.js API routes en su lugar?
Si tu app ya está en Next.js y el endpoint está estrechamente acoplado al frontend, sí — la co-ubicación es valiosa. Para servicios independientes, trabajos de larga duración, o cualquier cosa que necesite comportamiento de servidor personalizado (WebSockets, streaming, apagado elegante), un servidor Fastify o Express dedicado es más apropiado.
Relacionado
docs/nodejs-scripts/event-emitter.md—http.Serveres unEventEmitter, así que aplican los mismos patrones.docs/nextjs-routing/app-router.md— Manejadores de App Router de Next.js como alternativa para endpoints acoplados a aplicaciones.docs/nextjs-data/fetching.md— llamando APIs HTTP de los lados cliente y servidor de una app Next.js.