O Modelo de Servidor HTTP do Node.js
Todo framework HTTP que você usará no Node - Express, Fastify, NestJS - é uma camada de conveniências envolta no mesmo primitivo: o módulo node:http.
Busque em todas as páginas da documentação
Todo framework HTTP que você usará no Node - Express, Fastify, NestJS - é uma camada de conveniências envolta no mesmo primitivo: o módulo node:http.
Esse módulo não é um framework em si.
É uma tradução fina e deliberadamente de baixo nível de bytes TCP brutos em dois objetos JavaScript - uma requisição e uma resposta - e deixa quase todo o resto (roteamento, parsing, validação) para outra coisa adicionar.
Entender essa camada é importante mesmo que você nunca escreva http.createServer manualmente, porque o comportamento que você depurará em um framework - uma resposta travada, um erro de ordenação de cabeçalho, um timeout de cliente lento - é quase sempre um sintoma que vaza dessa camada.
Fundamentos de HTTP no Node oferece exemplos práticos deste módulo; esta página é o modelo por baixo desses exemplos - o que realmente são uma "requisição" e uma "conexão", e como os frameworks se posicionam por cima sem substituir nada disso.
http do Node transforma dados brutos de socket TCP em um IncomingMessage (a requisição) e ServerResponse (a resposta) baseados em streams, e todo framework nesta categoria roda como o callback conectado a esse mesmo evento de request de baixo nível.IncomingMessage, ServerResponse, keep-alive.Uma conexão e uma requisição não são a mesma coisa, e confundi-las é a fonte mais comum de confusão nesta camada.
Uma conexão é um socket TCP - um pipe bruto e persistente entre um cliente e o servidor.
Uma requisição é uma mensagem HTTP que viaja por esse socket.
Graças ao keep-alive do HTTP/1.1, um único socket rotineiramente carrega muitas requisições, uma após a outra, sem reabrir a conexão a cada vez - é por isso que os limites de conexão do lado do servidor e os limites por requisição são, de fato, botões de ajuste diferentes.
Quando um cliente abre uma conexão e envia uma mensagem HTTP bem formada, o parser HTTP embutido do Node (llhttp) transforma os bytes brutos em dois objetos e dispara um evento request no servidor: um IncomingMessage e um ServerResponse.
import { createServer } from "node:http";
const server = createServer((req, res) => {
// req: IncomingMessage - um stream legível do corpo da requisição
// res: ServerResponse - um stream gravável através do qual você envia a resposta
res.end("ok");
});Ambos os objetos são streams antes de serem qualquer outra coisa.
IncomingMessage é um stream legível - o corpo chega em pedaços (chunks), não como uma única string, porque o Node não tem ideia antecipada de quão grande será o corpo de uma requisição.
ServerResponse é um stream gravável - chamar res.write() envia bytes imediatamente se os cabeçalhos já foram enviados, ou os enfileira até que sejam.
Este design "stream-first" é o motivo pelo qual frameworks que "parseiam JSON automaticamente" estão, na verdade, apenas fazendo o trabalho de coleta de chunks mostrado em Fundamentos de HTTP no Node para você, por trás de uma chamada .json().
O caminho de um pacote TCP do cliente para sua função de tratamento tem uma forma fixa: o SO entrega uma conexão aceita ao net.Server, o http.Server anexa seu parser a esse socket, o parser decodifica incrementalmente os bytes em semânticas HTTP (método, caminho, cabeçalhos, chunks de corpo), e assim que uma linha de requisição e cabeçalhos completos chegam, o Node dispara o evento request com seu par IncomingMessage/ServerResponse.
Nada sobre este caminho é exclusivo do Express ou Fastify - ambos literalmente chamam http.createServer(listener) (ou um equivalente) nos bastidores, e passam sua própria função de listener como callback.
Socket TCP aceito
│
▼
Parser HTTP (llhttp) decodifica bytes incrementalmente
│
▼
Evento 'request' dispara ──────────────► listener do framework executa
│ (app Express, router Fastify, ...)
▼
Corpo chega como chunks de stream ao longo do tempo, independente do evento acimaO que difere entre frameworks é inteiramente o que acontece dentro dessa função de listener - o Express percorre uma cadeia de middleware, o Fastify despacha através de um router compilado com hooks de ciclo de vida, o NestJS resolve um grafo de módulos antes de delegar ao adaptador com o qual está configurado.
Nenhum deles recebe um objeto de requisição diferente do SO; todos decoram ou envolvem o mesmo par IncomingMessage/ServerResponse que o Node lhes entregou.
Essa é uma heurística útil para depuração: se algo quebra identicamente em todos os frameworks que você tenta, o bug está muito provavelmente nesta camada, não nas deles.
Os cabeçalhos merecem atenção especial porque o Node impõe uma regra de ordenação que confunde código do módulo bruto e do framework: res.writeHead() envia status e cabeçalhos juntos, imediatamente, enquanto res.setHeader() apenas enfileira um cabeçalho para ser enviado sempre que os primeiros bytes realmente saem (seja via primeiro write() ou via end()).
Uma vez que os cabeçalhos foram enviados - por qualquer um dos métodos - qualquer tentativa posterior de defini-los gera ERR_HTTP_HEADERS_SENT, que é exatamente o que acontece quando um handler responde duas vezes depois que um framework já enviou uma página de erro em seu nome.
Em escala, esta camada é onde residem os modos de falha em nível de conexão, e eles não desaparecem só porque você adicionou um framework por cima.
Um cliente lento ou malicioso que abre uma conexão e envia bytes lentamente pode manter um socket (e o handler esperando por ele) aberto muito mais tempo do que uma requisição legítima - server.requestTimeout e server.headersTimeout existem especificamente para limitar isso, e toda implantação de framework em produção deve configurá-los explicitamente em vez de confiar nos padrões (historicamente permissivos).
Proxies reversos mudam o que "o cliente" realmente significa nesta camada: um balanceador de carga ou CDN termina a conexão do cliente real e abre sua própria conexão para seu processo Node, então req.socket.remoteAddress reporta o IP do proxy, a menos que você confie e analise explicitamente X-Forwarded-For - veja Consciência de Proxy Reverso para os detalhes.
HTTP/2 e HTTP/3 mudam mais este modelo do que a maioria dos desenvolvedores espera: HTTP/2 multiplexa muitas requisições lógicas sobre uma única conexão TCP usando IDs de stream, o que quebra a simples intuição "um socket é aproximadamente igual ao tráfego de um cliente" que esta página tem construído - veja Considerações sobre HTTP/2 e HTTP/3.
| Abordagem | Força | Fraqueza | Melhor Encaixe |
|---|---|---|---|
node:http Bruto | Sem sobrecarga de dependência; controle total sobre cada byte | Sem roteamento, parsing ou validação - você constrói tudo | Sidecars de health-check, pequenas ferramentas internas, aprendendo o modelo |
Express sobre http | Enorme ecossistema; modelo mental simples sobre esta camada | Cadeia de middleware plana tem um teto de performance real em alto throughput | A maioria das APIs CRUD, equipes priorizando familiaridade |
Fastify sobre http | Serialização compilada por schema; estrutura de plugin encapsulada | Mais estrutura inicial do que um aplicativo pequeno pode precisar | APIs de alto throughput, equipes com foco em schema |
| NestJS (adaptador sobre Express/Fastify) | Arquitetura orientada a DI em escala; transporte agnóstico ao framework | Maior abstração sobre esta camada; curva de aprendizado mais íngreme | Grandes equipes, serviços empresariais de longa duração |
A observabilidade nesta camada é barata e frequentemente ignorada: os eventos request, close e clientError no http.Server permitem medir a saúde em nível de conexão (requisições malformadas, desconexões abruptas) independentemente do que o logging próprio do seu framework faz, o que é útil precisamente porque não é filtrado pela lógica de roteamento em nível de framework.
keepAliveTimeout) e configurações em nível de requisição (como requestTimeout) governam coisas diferentes.http do Node." Eles o envolvem - cada app Express, Fastify ou NestJS-on-Express ainda é uma chamada http.createServer com uma função de listener mais elaborada dentro.req.body no Express) é um framework bufferizando e parseando esses chunks para você primeiro.res.end() é opcional se você já chamou res.write()." O cliente continua esperando até que end() seja chamado (ou a conexão expire) - uma resposta não é "enviada" até que seja explicitamente fechada.write() ou end()), novas alterações de cabeçalho geram ERR_HTTP_HEADERS_SENT.A camada de tradução embutida do Node entre bytes brutos de socket TCP e dois objetos JavaScript baseados em streams - um IncomingMessage (requisição) e um ServerResponse (resposta) - sobre os quais todo framework HTTP constrói suas próprias abstrações.
Não. Express e Fastify chamam http.createServer() (ou o equivalente HTTPS/HTTP2) diretamente; NestJS delega ao adaptador que você configurar, que por si só é Express ou Fastify por baixo. Todos recebem o mesmo par IncomingMessage/ServerResponse do Node.
O Node não tem como saber o tamanho total de um corpo antecipadamente, então ele o entrega como um stream legível de chunks à medida que chegam pela rede. req.body só existe porque um framework (ou seu próprio código) coletou e parseou esses chunks primeiro.
O socket TCP permanece aberto após uma resposta terminar, então a próxima requisição do mesmo cliente pode reutilizá-lo em vez de arcar com o custo de um novo handshake TCP. server.keepAliveTimeout controla por quanto tempo o Node mantém esse socket ocioso aberto esperando por uma próxima requisição.
Quase sempre uma res.end() faltando ou inalcançável - um stream de resposta que nunca é explicitamente fechado deixa o cliente (e a conexão) esperando indefinidamente, ou até que um timeout intervenha.
writeHead() envia status e cabeçalhos imediatamente, como uma única ação. setHeader() apenas enfileira um valor de cabeçalho para ser incluído sempre que a resposta realmente começar a ser enviada - a primeira chamada write() ou a chamada end(), o que ocorrer primeiro.
Porque um proxy reverso ou balanceador de carga geralmente termina a conexão do cliente real e abre sua própria conexão para seu processo Node - então o endereço em nível de socket é o do proxy, não o do cliente original, a menos que você confie e analise explicitamente um cabeçalho forwarded-for.
Sim, para serviços pequenos e de propósito único - endpoints de health-check, sidecars internos, ou qualquer coisa onde roteamento e parsing de corpo adicionam mais sobrecarga do que valor. A maioria das APIs de aplicação ainda se beneficia da estrutura de um framework quando crescem além de um punhado de rotas.
Ele multiplexa múltiplas trocas lógicas de requisição/resposta sobre uma única conexão TCP usando IDs de stream, em vez de uma requisição terminar antes que a próxima comece - o que quebra a intuição simples "uma conexão mapeia aproximadamente para uma requisição por vez" que esta página constrói em torno do HTTP/1.1.
Que o código em algum lugar tentou definir um cabeçalho ou código de status depois que a resposta já começou a ser enviada - geralmente um sinal de um handler executando duas vezes, ou um caminho de erro de fallback executando após uma resposta normal já ter sido concluída.
Não - todos eles leem o mesmo stream subjacente, mas diferem nos padrões (limites de tamanho, tratamento de content-type) e em quando o parsing acontece no pipeline. O Express exige express.json() explicitamente; o Fastify inclui parsing JSON por padrão com limites conscientes de schema.
Para limitar quanto tempo um cliente lento ou mal-comportado pode manter uma conexão (e os recursos do servidor por trás dela) aberta, em vez de confiar em padrões que historicamente permitiam esperas quase ilimitadas - um vetor real para exaustão de conexões sob carga.
createServer, cabeçalhos e streamingVersões do Stack: Esta página foi escrita para Node.js 24.18.0 (Active LTS), npm 10+, e TypeScript 5.6+.
Revisado por Chris St. John·Última atualização: 15 de jul. de 2026