Creando servidores HTTP
Tres formas de crear servidores HTTP en Node.js - node:http sin dependencias, Express por familiaridad, Fastify por rendimiento.
Busca en todas las páginas de la documentación
Tres formas de crear servidores HTTP en Node.js - node:http sin dependencias, Express por familiaridad, Fastify por rendimiento.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
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:http te obliga a manejar enrutamiento, análisis del cuerpo y códigos de estado por ti mismo - mínimo pero verboso.express.json()) te da req.body de forma gratuita y utiliza semántica familiar req/res.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.
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.
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.
res.end() en node:http. La solicitud se cuelga hasta que el cliente agota el tiempo. Cada ruta de código debe llamar a res.end() (o res.writeHead().end()), incluyendo ramas de error.express.json() después de una ruta significa que req.body es undefined. CORS después de rutas significa que las solicitudes preflight devuelven 404. Manejadores de error antes de rutas nunca se ejecutan.OPTIONS antes de solicitudes no simples. Si tu router solo registra GET/POST, preflight falla. Usa un middleware CORS que maneje OPTIONS globalmente.EADDRINUSE al reiniciar. El proceso anterior aún mantiene el puerto. Mátalo, espera la ventana TIME_WAIT, o establece server.listen(\{ port, exclusive: false \}) - llaves escapadas en prosa.return y res.send en Express. Devolver un valor no hace nada; Express solo responde cuando llamas a res.send/res.json. En Fastify, lo opuesto - devuelve el valor y se envía automáticamente.| 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. |
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
docs/nodejs-scripts/event-emitter.md - http.Server es un EventEmitter, 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.Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥