Roteamento Sem Frameworks
Combine URLs e métodos com http.createServer puro quando um framework completo adiciona mais complexidade do que valor.
Receita
Cartão de receita de referência rápida - pronto para copiar e colar.
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();
});Quando usar isso: Verificações de saúde, portas de administração internas, sidecars ou serviços com menos de 10 endpoints onde adicionar Express é apenas peso de dependência.
Exemplo de Trabalho
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);O que isso demonstra:
- Parâmetros de caminho via conversão de regex (
:idpara capturar grupos) - Roteamento ciente de método sem uma biblioteca de roteador
- Padrão de retorno antecipado para evitar respostas duplas
- Uma tabela de roteador que você pode testar unitariamente independentemente do HTTP
Análise Profunda
Como Funciona
- O Node passa cada requisição para um único callback - o roteamento é sua responsabilidade
req.urlé um caminho de string mais a query; analise comURLpara extração confiável do nome do caminho- A correspondência de rotas é O(n) sobre uma lista linear - bom para tabelas de rotas pequenas, lento com centenas de rotas
- Frameworks adicionam roteadores baseados em trie, cadeias de middleware e coerção de parâmetros
Quando o Roteamento Puro Vence
| Cenário | HTTP Puro | Framework |
|---|---|---|
Liveness/readiness do K8s em :8081 | Sim | Exagero |
| Receptor de webhook de 3 endpoints | Sim | Opcional |
| API REST pública com autenticação, validação, OpenAPI | Não | Sim |
| Equipe de 5+ na mesma base de código | Não | Sim (convenções importam) |
Notas de TypeScript
// Restringe o método na fronteira
const ALLOWED = new Set(["GET", "POST", "PUT", "DELETE", "PATCH"]);
if (!ALLOWED.has(req.method ?? "")) {
res.writeHead(405, { Allow: [...ALLOWED].join(", ") }).end();
return;
}Armadilhas
- Strings de consulta nas chaves de rota -
req.urlé/users?page=1, não/users. Analise o nome do caminho antes de corresponder. Correção: usenew URL(req.url, base).pathname. - Barras finais -
/userse/users/são caminhos diferentes. Correção: normalize em uma função estilo middleware. - Sem 405 automático - Método não correspondente em um caminho existente retorna 404, a menos que você verifique o método separadamente. Correção: correspondência em duas passagens: primeiro o caminho, depois o método.
- Erros assíncronos não capturados - Manipuladores brutos não capturam rejeições de promessa. Correção: envolva em try/catch ou use Express 5 / Fastify.
- Análise de corpo duplicada - Cada rota POST precisa de seu próprio analisador. Correção: extraia uma função auxiliar
readJson(req)com limites de tamanho. - Sem CORS integrado - Navegadores bloquearão chamadas cross-origin. Correção: adicione um manipulador OPTIONS ou use middleware de framework.
Alternativas
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| Express 5 | A equipe o conhece, plugins de ecossistema necessários | A vazão máxima é a principal restrição |
| Fastify 5 | Validação de esquema e velocidade importam | A equipe não tem experiência com Fastify e tem um prazo apertado |
| Hono | Pegada minúscula, portabilidade de borda | Grande DI estilo Nest e decoradores necessários |
| find-my-way (standalone) | Quer o roteador do Fastify sem o framework | Você também precisa de middleware, análise e plugins |
FAQs
Quantas rotas antes de eu deveria usar um framework?
Não há um número mágico. O ponto de virada geralmente é a complexidade: quando você precisa de ordenação de middleware, tratamento de erros consistente, validação de requisição e convenções de equipe, um framework se paga em torno de 10-15 endpoints.
Posso usar aliases de caminho do `node:http` com TypeScript?
Sim, mas a camada HTTP não se importa com a estrutura do seu projeto. Mantenha as definições de rota em um módulo routes.ts dedicado de qualquer maneira.
Verificações de saúde devem compartilhar o servidor principal?
Padrão comum: aplicativo principal em :3000, saúde em :8081 com um manipulador de 3 linhas. Isola o tráfego de sonda da autenticação e do middleware do aplicativo.
Como testar rotas sem iniciar uma porta?
Extraia as funções match() e os manipuladores como unidades puras. Para testes de integração, use server.listen(0) para obter uma porta aleatória ou mude para Fastify inject().
O roteamento com regex é seguro?
ReDoS é um risco com padrões complexos fornecidos pelo usuário. Use modelos de rota fixos (como segmentos :id) e valide os formatos de parâmetro nos manipuladores.
E a versionamento REST em HTTP puro?
Corresponda a /v1/users como um prefixo de caminho em sua tabela de rotas, ou inspecione um cabeçalho Accept-Version. Roteadores de framework suportam caminhos de montagem (/v1) de forma mais ergonômica.
Posso compartilhar a lógica de roteamento entre HTTP e WebSocket?
Sim - analise a URL uma vez e, em seguida, ramifique no cabeçalho Upgrade. Veja Noções Básicas de Tempo Real.
O Node 24 tem um roteador integrado?
Não. http.createServer é intencionalmente mínimo. Use um framework ou uma biblioteca focada como find-my-way.
Relacionados
- Noções Básicas de HTTP em Node - ciclo de vida de requisição/resposta
- Padrão de Middleware - pipelines compostos
- Noções Básicas de Express - quando atualizar
- Noções Básicas de Hono - alternativa de framework mínima
- Lista de Verificação de Seleção de Framework - escolha a ferramenta certa
Versões da Stack: Esta página foi escrita para Node.js 24.18.0 (LTS Ativo), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 e NestJS 11.