Enrutamiento sin frameworks
Haz coincidir URLs y métodos con http.createServer cuando un framework completo añade más complejidad que valor.
Receta
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
import { createServer } from "node:http";
type Handler = (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse) => void;
const routes = new Map<string, Handler>([
["GET /health", (_req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok" }));
}],
["GET /users", (_req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify([{ id: 1 }]));
}],
]);
const server = createServer((req, res) => {
const key = `${req.method} ${new URL(req.url ?? "/", "http://localhost").pathname}`;
const handler = routes.get(key);
if (handler) return handler(req, res);
res.writeHead(404).end();
});Cuándo usarlo: Comprobaciones de salud, puertos de administración internos, sidecars o servicios con menos de 10 endpoints donde añadir Express es puro peso de dependencia.
Ejemplo de trabajo
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
type RouteHandler = (req: IncomingMessage, res: ServerResponse, params: Record<string, string>) => void;
interface Route {
method: string;
pattern: RegExp;
paramNames: string[];
handler: RouteHandler;
}
function route(method: string, path: string, handler: RouteHandler): Route {
const paramNames: string[] = [];
const pattern = new RegExp(
"^" + path.replace(/:([a-zA-Z]+)/g, (_, name) => {
paramNames.push(name);
return "([^/]+)";
}) + "$"
);
return { method, pattern, paramNames, handler };
}
const routes: Route[] = [
route("GET", "/users/:id", (req, res, params) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ id: params.id }));
}),
route("DELETE", "/users/:id", (req, res, params) => {
res.writeHead(204).end();
console.log(`Deleted user ${params.id}`);
}),
];
function match(req: IncomingMessage): { handler: RouteHandler; params: Record<string, string> } | null {
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
for (const r of routes) {
if (r.method !== req.method) continue;
const m = pathname.match(r.pattern);
if (!m) continue;
const params: Record<string, string> = {};
r.paramNames.forEach((name, i) => { params[name] = m[i + 1]; });
return { handler: r.handler, params };
}
return null;
}
const server = createServer((req, res) => {
const found = match(req);
if (found) return found.handler(req, res, found.params);
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
});
server.listen(3000);Lo que esto demuestra:
- Parámetros de ruta mediante conversión de regex (de
:ida grupos de captura) - Enrutamiento consciente del método sin una librería de enrutamiento
- Patrón de retorno anticipado para evitar respuestas dobles
- Una tabla de enrutamiento que puedes probar unitariamente independientemente de HTTP
Análisis profundo
Cómo funciona
- Node pasa cada solicitud a una única función de callback; el enrutamiento es tu responsabilidad
req.urles una cadena de ruta más la consulta; analízala conURLpara una extracción fiable del pathname- La coincidencia de rutas es O(n) sobre una lista lineal; está bien para tablas de rutas pequeñas, lenta para cientos de rutas
- Los frameworks añaden enrutadores basados en árboles de prefijos (trie), cadenas de middleware y coerción de parámetros
Cuándo el enrutamiento simple gana
| Escenario | HTTP simple | Framework |
|---|---|---|
Liveness/readiness de K8s en :8081 | Sí | Excesivo |
| Receptor de webhook de 3 endpoints | Sí | Opcional |
| API REST pública con autenticación, validación, OpenAPI | No | Sí |
| Equipo de más de 5 personas en la misma base de código | No | Sí (las convenciones importan) |
Notas de TypeScript
// Limitar el método en el límite
const ALLOWED = new Set(["GET", "POST", "PUT", "DELETE", "PATCH"]);
if (!ALLOWED.has(req.method ?? "")) {
res.writeHead(405, { Allow: [...ALLOWED].join(", ") }).end();
return;
}Errores comunes
- Cadenas de consulta en las claves de ruta -
req.urles/users?page=1, no/users. Analiza el pathname antes de hacer coincidir. Solución: usanew URL(req.url, base).pathname. - Barras diagonales finales -
/usersy/users/son rutas diferentes. Solución: normaliza en una función de estilo middleware. - No hay 405 automático - Un método no coincidente en una ruta existente devuelve 404 a menos que verifiques el método por separado. Solución: coincidencia en dos pasadas: ruta primero, luego método.
- Errores asíncronos no capturados - Los manejadores sin procesar no capturan las promesas rechazadas. Solución: envuelve en try/catch o usa Express 5 / Fastify.
- Análisis del cuerpo duplicado - Cada ruta POST necesita su propio analizador. Solución: extrae una función auxiliar
readJson(req)con límites de tamaño. - No hay CORS incorporado - Los navegadores bloquearán las llamadas de origen cruzado. Solución: añade un manejador OPTIONS o usa middleware de framework.
Alternativas
| Alternativa | Usar cuándo | No usar cuándo |
|---|---|---|
| Express 5 | El equipo lo conoce, se necesitan plugins del ecosistema | El rendimiento máximo es la principal restricción |
| Fastify 5 | La validación de esquemas y la velocidad importan | El equipo no tiene experiencia con Fastify y el plazo es ajustado |
| Hono | Huella mínima, portabilidad en el edge | Se requiere DI y decoradores pesados al estilo Nest |
| find-my-way (autónomo) | Quieres el enrutador de Fastify sin el framework | También necesitas middleware, análisis y plugins |
Preguntas frecuentes
¿Cuántas rutas antes de que deba usar un framework?
No hay un número mágico. El punto de inflexión suele ser la complejidad: cuando necesitas orden de middleware, manejo consistente de errores, validación de solicitudes y convenciones de equipo, un framework se amortiza con alrededor de 10-15 endpoints.
¿Puedo usar `node:http` con alias de ruta de TypeScript?
Sí, pero la capa HTTP no se preocupa por la estructura de tu proyecto. Mantén las definiciones de ruta en un módulo routes.ts dedicado de cualquier manera.
¿Las comprobaciones de salud deben compartir el servidor principal?
Patrón común: aplicación principal en :3000, salud en :8081 con un manejador de 3 líneas. Aísla el tráfico de sondeo del middleware de la aplicación y la autenticación.
¿Cómo pruebo las rutas sin iniciar un puerto?
Extrae las funciones match() y del manejador como unidades puras. Para pruebas de integración, usa server.listen(0) para obtener un puerto aleatorio, o cambia a inject() de Fastify.
¿Es seguro el enrutamiento con expresiones regulares?
ReDoS es un riesgo con patrones complejos suministrados por el usuario. Usa plantillas de ruta fijas (como segmentos :id) y valida los formatos de los parámetros en los manejadores.
¿Qué pasa con el versionado REST en HTTP simple?
Haz coincidir /v1/users como un prefijo de ruta en tu tabla de rutas, o inspecciona un encabezado Accept-Version. Los enrutadores de frameworks admiten rutas de montaje (/v1) de forma más ergonómica.
¿Puedo compartir la lógica de enrutamiento entre HTTP y WebSocket?
Sí, analiza la URL una vez y luego ramifica según el encabezado Upgrade. Consulta Conceptos básicos en tiempo real.
¿Node 24 tiene un enrutador incorporado?
No. http.createServer es intencionalmente mínimo. Usa un framework o una librería enfocada como find-my-way.
Relacionado
- Conceptos básicos de HTTP en Node - ciclo de vida de solicitud/respuesta
- Patrón Middleware - pipelines componibles
- Conceptos básicos de Express - cuándo actualizar
- Conceptos básicos de Hono - alternativa de framework mínima
- Lista de verificación para la selección de frameworks - elige la herramienta adecuada
Versiones de la pila: Esta página fue escrita para Node.js 24.18.0 (LTS activa), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 y NestJS 11.