Pesquisa de site

Como Criar um Servidor Web em Node.js com o Módulo HTTP


Introdução

Quando você visualiza uma página da web no seu navegador, você está fazendo uma requisição para outro computador na internet, que então fornece a página da web como resposta. Esse computador com o qual você está se comunicando via internet é um servidor web. Um servidor web recebe requisições HTTP de um cliente, como o seu navegador, e fornece uma resposta HTTP, como uma página HTML ou JSON de uma API.

O módulo http do Node.js fornece tudo o que você precisa para construir um servidor web sem dependências externas. Ele lida com solicitações recebidas, analisa cabeçalhos HTTP, gerencia conexões e envia respostas—tudo por meio de APIs JavaScript simples.

O módulo http é a solução integrada do Node.js para criar servidores e clientes HTTP. Diferente de frameworks como o Express.js, ele faz parte do núcleo do Node.js, ou seja, não é necessário instalar nada nem depender de outras bibliotecas. Isso o torna ideal para aplicações leves, para aprender os fundamentos do HTTP ou quando você quer ter controle total sobre o tratamento de requisições.

Muito software está envolvido para que um servidor retorne uma página da web. Esse software geralmente se divide em duas categorias: frontend e backend. Código de front-end preocupa-se com a forma como o conteúdo é apresentado, como a cor de uma barra de navegação e o estilo do texto. Código de back-end preocupa-se com a forma como os dados são trocados, processados e armazenados. Código que lida com requisições de rede do seu navegador ou se comunica com o banco de dados é gerenciado principalmente pelo código de back-end.

O Node.js permite que os desenvolvedores usem JavaScript para escrever código de back-end, embora tradicionalmente ele fosse usado no navegador para escrever código de front-end. Ter o front-end e o back-end juntos assim reduz o esforço necessário para criar um servidor web, o que é uma das principais razões pelas quais o Node.js é uma escolha popular para escrever código de back-end.

Este tutorial demonstra como construir servidores web prontos para produção usando o módulo http incluído no Node.js. Você vai criar servidores que lidam com roteamento, analisam o corpo das requisições, servem arquivos estáticos, implementam tratamento de erros adequado e funcionam tanto com a sintaxe CommonJS quanto com a ES Module.

Simplifique a implantação de aplicações Node.js com a Plataforma de Aplicativos Linux-Console.net. Implante o Node diretamente do GitHub em minutos.

Principais Conclusões

Antes de começar, veja o que você vai conquistar:

  • Crie servidores HTTP usando o módulo http do Node.js sem frameworks:
    Você aprenderá a criar um servidor web HTTP totalmente funcional usando apenas o módulo http do Node.js. Isso significa evitar dependências externas ou frameworks, proporcionando uma compreensão mais profunda de como os servidores realmente funcionam em um nível baixo, além de permitir implementações mais leves e flexíveis para seus casos de uso.

  • Manipule diferentes tipos de conteúdo (JSON, HTML, CSV, arquivos estáticos) com cabeçalhos adequados:
    Você vai descobrir como trabalhar com múltiplos tipos de conteúdo, respondendo com HTML para páginas web, JSON para APIs, arquivos CSV ou servindo recursos estáticos como imagens e scripts. Você vai entender como configurar corretamente os cabeçalhos HTTP como Content-Type, garantindo que navegadores e clientes interpretem suas respostas de forma correta, proporcionando uma experiência de usuário fluida.

  • Implemente roteamento usando análise da URL da requisição e declarações switch:
    O roteamento é fundamental para servidores web, permitindo que você forneça lógica diferente para URLs e endpoints diversos. Você verá técnicas para analisar URLs de requisições recebidas e estruturar a lógica do seu servidor (por exemplo, com declarações switch) para fornecer respostas personalizadas com base em caminhos e métodos HTTP, simulando o básico do que frameworks maiores fazem nos bastidores.

  • Processar requisições POST analisando os corpos das requisições e lidando com diferentes tipos de conteúdo:
    Lidar com dados enviados pelo usuário é fundamental para qualquer aplicação interativa. Você aprenderá como tratar requisições POST, incluindo ler e analisar o corpo da requisição como dados brutos, JSON ou outros formatos. Esta seção aborda como armazenar temporariamente os dados recebidos, analisá-los de maneira segura e responder de acordo com o conteúdo recebido, fornecendo os blocos de construção para interação do usuário ou desenvolvimento de APIs.

  • Servir arquivos estáticos de forma eficiente a partir do sistema de arquivos:
    Muitos servidores web precisam servir recursos estáticos—imagens, folhas de estilo, arquivos JavaScript—diretamente para os usuários. Você vai descobrir como ler arquivos do disco e enviá-los de forma eficiente via HTTP, gerenciando os caminhos dos arquivos de maneira responsável para evitar problemas de segurança, e explorando otimizações como streaming de arquivos para melhorar o desempenho de arquivos maiores.

  • Implemente o tratamento de erros com códigos de status HTTP apropriados e registro de logs:
    Um servidor robusto deve lidar com erros de forma graciosa. Você verá como detectar, registrar e responder a erros—como solicitações incorretas ou arquivos ausentes—usando códigos de status HTTP padronizados como 400, 404 e 500. Você também aprenderá práticas básicas de registro de logs para que os problemas possam ser rastreados e os usuários recebam mensagens de erro informativas sem expor informações sensíveis.

  • Use JavaScript moderno com as sintaxes CommonJS (require) e ES Modules (import):
    O Node.js suporta dois sistemas de módulos: CommonJS e ES Modules. Você praticará o uso de require() e declarações import, entendendo quando — e como — usar cada estilo. Isso não apenas garante compatibilidade entre diferentes projetos Node.js, mas também ajuda você a escrever código JavaScript moderno, limpo e fácil de manter.

  • Aplicar as melhores práticas de produção, incluindo cache de arquivos, recuperação de erros e considerações de segurança:
    Por fim, você abordará as melhores práticas essenciais para implantar servidores web Node.js em ambientes reais. Isso inclui aproveitar o cache de arquivos para desempenho, implementar estratégias de recuperação de erros para manter seu servidor resiliente, além de aplicar medidas básicas de segurança (como sanitização de entradas e cabeçalhos HTTP apropriados) para proteger suas aplicações e usuários.

Entendendo o módulo HTTP

O módulo http é a interface de baixo nível do Node.js para comunicação HTTP. Ele fornece:

  • Criação de servidor: http.createServer() cria uma instância de servidor HTTP
  • Manipulação de requisições: As requisições recebidas são representadas por objetos IncomingMessage
  • Gerenciamento de respostas: As respostas de saída usam objetos ServerResponse
  • Suporte a streaming: Tanto as requisições quanto as respostas são streams, permitindo a transferência eficiente de dados

Por que usar o módulo HTTP em vez do Express? Embora o Express.js forneça abstrações convenientes, o módulo nativo http oferece controle completo, tamanhos de pacotes menores e entendimento profundo dos fundamentos do HTTP. É a base sobre a qual os frameworks são construídos. Para APIs simples, microsserviços ou fins de aprendizado, muitas vezes é tudo o que você precisa.

O módulo é orientado a eventos: quando uma solicitação chega, o servidor emite um evento 'request' e chama sua função de listener de solicitações. Este design permite que o Node.js lide de forma eficiente com muitas conexões simultâneas em uma única thread através do seu loop de eventos.

Conceitos principais para entender:

  • E/S não bloqueante: O http módulo usa o loop de eventos do Node.js para gerenciar múltiplas conexões sem criar threads separadas para cada requisição. Isso o torna altamente eficiente para operações dependentes de I/O.
  • Streams: Objetos de requisição e resposta herdam de streams do Node.js. Isso significa que grandes cargas úteis podem ser processadas em partes, sem carregar tudo na memória—essencial para lidar com upload de arquivos ou servir arquivos grandes.
  • Cabeçalhos e códigos de status: O módulo lhe dá controle direto sobre os cabeçalhos HTTP (res.setHeader()) e códigos de status (res.writeHead()), permitindo uma semântica HTTP precisa.
  • Sem roteamento mágico: Ao contrário dos frameworks, não há roteamento incorporado. Você analisa manualmente req.url e req.method para determinar como responder—oferecendo total transparência de como o roteamento realmente funciona.

Quando você chama http.createServer(), o Node.js cria um servidor TCP nos bastidores que escuta conexões recebidas, analisa mensagens do protocolo HTTP e emite eventos que seu código pode manipular. Entender essa operação de baixo nível ajuda a depurar problemas e tomar decisões arquitetônicas informadas.

Pré-requisitos

  • Certifique-se de que o Node.js esteja instalado em sua máquina de desenvolvimento. Este tutorial funciona com Node.js 20.x ou 22.x LTS (recomendado). Para instalar o Node.js no macOS ou Ubuntu, siga os passos em Como Instalar o Node.js e Criar um Ambiente de Desenvolvimento Local no macOS ou Como Instalar o Node.js no Ubuntu.

  • A plataforma Node.js suporta a criação de servidores web de forma nativa. Ter familiaridade com os conceitos básicos do Node.js é útil. Confira nosso guia sobre Como Escrever e Executar Seu Primeiro Programa em Node.js.

  • A programação assíncrona é usada em algumas seções. Se você é novo em código assíncrono ou no módulo fs, veja Como Escrever Código Assíncrono no Node.js.

  • Para seções focadas em produção, a familiaridade com gerenciamento de processos no Linux (systemd/PM2) e proxies reversos (como Nginx) ajudará na aplicação das orientações de implantação.

Passo 1 — Criando um Servidor HTTP Básico

Construa um servidor HTTP minimalista que responda a qualquer requisição com texto simples. Esta base se aplica tanto se você usar o Express quanto se construir do zero. Quando o Node.js chama http.createServer(), ele cria um servidor TCP que escuta em uma porta. Cada conexão recebida dispara a função de listener de requisição.

Configure sua pasta de projeto:

mkdir first-servers

cd first-servers

Agora, crie o arquivo que abrigará o código:

touch hello.js

Abra o arquivo em um editor de texto. Usaremos o nano, pois ele está disponível no terminal:

nano hello.js

Começamos carregando o módulo http que é padrão em todas as instalações do Node.js. Adicione a seguinte linha em hello.js:

const http = require("http");

O módulo http contém a função para criar o servidor, que veremos mais adiante. Se você quiser saber mais sobre módulos no Node.js, confira nosso artigo Como Criar um Módulo Node.js.

Nosso próximo passo será definir duas constantes, o host e a porta aos quais nosso servidor estará vinculado:

...
const host = 'localhost';
const port = 8000;

Como mencionado anteriormente, os servidores web aceitam solicitações de navegadores e outros clientes. Podemos interagir com um servidor web digitando um nome de domínio, que é traduzido para um endereço IP por um servidor DNS. Um endereço IP é uma sequência única de números que identifica uma máquina em uma rede, como a internet. Para mais informações sobre conceitos de nomes de domínio, confira nosso artigo Uma Introdução à Terminologia, Componentes e Conceitos de DNS.

O valor localhost é um endereço privado especial que os computadores usam para se referirem a si mesmos. Normalmente, é o equivalente ao endereço IP interno 127.0.0.1 e só está disponível para o computador local, não para nenhuma rede local à qual estejamos conectados nem para a internet.

A porta é um número que os servidores usam como um ponto de extremidade ou "porta" para nosso endereço IP. Em nosso exemplo, usaremos a porta 8000 para nosso servidor web. As portas 8080 e 8000 são tipicamente usadas como portas padrão em desenvolvimento, e na maioria dos casos os desenvolvedores as usarão em vez de outras portas para servidores HTTP.

Quando vinculamos nosso servidor a este host e porta, poderemos acessar nosso servidor visitando http://localhost:8000 em um navegador local.

Vamos adicionar uma função especial, que no Node.js chamamos de listener de requisição. Essa função serve para lidar com uma requisição HTTP recebida e retornar uma resposta HTTP. Essa função deve ter dois argumentos, um objeto de requisição e um objeto de resposta. O objeto de requisição captura todos os dados da requisição HTTP que está chegando. O objeto de resposta é usado para retornar respostas HTTP para o servidor.

Queremos que nosso primeiro servidor retorne esta mensagem sempre que alguém acessá-lo: "Meu primeiro servidor!".

Vamos adicionar essa função a seguir:

...

const requestListener = function (req, res) {
  res.writeHead(200);
  res.end("My first server!");
};

A função geralmente seria nomeada com base no que ela faz. Por exemplo, se criássemos uma função de escuta de requisições para retornar uma lista de livros, provavelmente a nomearíamos listBooks(). Como este é um caso de exemplo, usaremos o nome genérico requestListener.

Todas as funções listener de requisições no Node.js aceitam dois argumentos: req e res (podemos nomeá-los de forma diferente se quisermos). A requisição HTTP que o usuário envia é capturada em um objeto Request, que corresponde ao primeiro argumento, req. A resposta HTTP que retornamos ao usuário é formada ao interagir com o objeto Response no segundo argumento, res.

A primeira linha res.writeHead(200); define o código de status HTTP da resposta. Os códigos de status HTTP indicam quão bem uma solicitação HTTP foi tratada pelo servidor. Neste caso, o código de status 200 corresponde a "OK". Se você estiver interessado em aprender sobre os diversos códigos HTTP que seus servidores web podem retornar e o significado que eles representam, nosso guia sobre Como Solucionar Problemas Comuns de Códigos de Erro HTTP é um bom lugar para começar.

A próxima linha da função, res.end("Meu primeiro servidor!");, escreve a resposta HTTP de volta para o cliente que a solicitou. Essa função retorna quaisquer dados que o servidor precise enviar. Neste caso, ela está retornando dados em texto.

Finalmente, agora podemos criar nosso servidor e usar nosso ouvinte de requisições:

...

const server = http.createServer(requestListener);
server.listen(port, host, () => {
  console.log(`Server is running on http://${host}:${port}`);
});

Salve e saia do nano pressionando CTRL+X.

Na primeira linha, criamos um novo objeto server através da função createServer() do módulo http. Esse servidor aceita requisições HTTP e as encaminha para nossa função requestListener().

Depois de criarmos nosso servidor, devemos vinculá-lo a um endereço de rede. Fazemos isso com o método server.listen(). Ele aceita três argumentos: port, host e uma função de callback que é executada quando o servidor começa a escutar.

Todos esses argumentos são opcionais, mas é uma boa ideia declarar explicitamente qual porta e host queremos que um servidor web use. Ao implantar servidores web em diferentes ambientes, saber a porta e o host em que ele está sendo executado é necessário para configurar balanceamento de carga ou um alias de DNS.

A função de callback registra uma mensagem em nosso console para que possamos saber quando o servidor começou a ouvir conexões.

Aqui está o arquivo completo hello.js com comentários inline explicando cada parte:

// Import the built-in http module - no installation needed
const http = require("http");

// Define where the server will listen
const host = 'localhost';  // Only accessible from this machine
const port = 8000;          // Common development port

// Request listener: handles every incoming HTTP request
const requestListener = function (req, res) {
    // Set HTTP status code to 200 (OK)
    res.writeHead(200);
    
    // Send the response body and close the connection
    res.end("My first server!");
};

// Create the server with our request listener
const server = http.createServer(requestListener);

// Start listening for connections
server.listen(port, host, () => {
    console.log(`Server is running on http://${host}:${port}`);
});

Este código cria um servidor HTTP funcional em menos de 20 linhas. Toda vez que um cliente se conecta — independentemente da URL ou do método HTTP — o servidor responde com “Meu primeiro servidor!” e um código de status 200.

Nota: Mesmo que requestListener() não use o objeto req, ele ainda deve ser o primeiro argumento da função.

Com menos de quinze linhas de código, agora temos um servidor web. Vamos vê-lo em ação e testá-lo de ponta a ponta executando o programa:

node hello.js

No console, veremos esta saída:

Server is running on http://localhost:8000

Observe que o prompt desaparece. Isso acontece porque um servidor Node.js é um processo de longa duração. Ele só é encerrado se encontrar um erro que faça com que ele falhe e saia, ou se pararmos o processo do Node.js que está executando o servidor.

Em uma janela de terminal separada, vamos nos comunicar com o servidor usando o cURL, uma ferramenta de linha de comando para transferir dados de e para uma rede. Digite o comando para fazer uma requisição HTTP GET ao nosso servidor em execução:

curl http://localhost:8000

Quando pressionamos ENTER, nosso terminal mostrará a seguinte saída:

My first server!

Agora configuramos um servidor e recebemos nossa primeira resposta do servidor.

Vamos analisar o que aconteceu quando testamos nosso servidor. Usando cURL, enviamos uma requisição GET para o servidor em http://localhost:8000. Nosso servidor Node.js escutou conexões desse endereço. O servidor passou essa requisição para a função requestListener(). A função retornou dados de texto com o código de status 200. Em seguida, o servidor enviou essa resposta de volta para o cURL, que exibiu a mensagem em nosso terminal.

Em produção, você normalmente não se vincula a localhost—você se vincula a 0.0.0.0 para aceitar conexões de qualquer interface de rede. No entanto, para desenvolvimento local, localhost é mais seguro e evita acesso externo ao seu servidor de desenvolvimento. Além disso, se a porta 8000 já estiver em uso, você receberá um erro EADDRINUSE—simplesmente escolha outra porta ou pare o processo que está usando essa porta com lsof -i :8000 (macOS/Linux).

Antes de continuarmos, vamos sair do nosso servidor em execução pressionando CTRL+C. Isso interrompe a execução do nosso servidor, nos trazendo de volta ao prompt de comando.

Na maioria dos sites que visitamos ou APIs que usamos, as respostas do servidor raramente estão em texto simples. Recebemos páginas HTML e dados JSON como formatos de resposta comuns. No próximo passo, aprenderemos como retornar respostas HTTP nos formatos de dados comuns que encontramos na web.

Se você quer aprender a fazer requisições HTTP a partir do Node.js (lado cliente), confira nosso guia sobre Como Criar um Cliente HTTP com o Core HTTP no Node.js.

Passo 2 — Retornando Diferentes Tipos de Conteúdo

A resposta que retornamos de um servidor web pode ter uma variedade de formatos. JSON e HTML foram mencionados anteriormente, e também podemos retornar outros formatos de texto como XML e CSV. Finalmente, servidores web podem retornar dados não textuais, como PDFs, arquivos compactados, áudio e vídeo.

Neste artigo, além do texto simples que acabamos de retornar, você aprenderá como retornar os seguintes tipos de dados:

  • JSON
  • CSV
  • HTML

Os três tipos de dados são todos baseados em texto e são formatos populares para entrega de conteúdo na web. Muitas linguagens de desenvolvimento do lado do servidor e ferramentas oferecem suporte para retornar esses diferentes tipos de dados. No contexto do Node.js, precisamos fazer duas coisas:

  1. Defina o cabeçalho Content-Type em nossas respostas HTTP com o valor apropriado.
  2. Certifique-se de que res.end() receba os dados no formato correto.

Vamos ver isso em ação com alguns exemplos. O código que escreveremos nesta seção e nas próximas possui muitas semelhanças com o código que escrevemos anteriormente. A maioria das mudanças está dentro da função requestListener(). Vamos criar arquivos com este “código modelo” para tornar as seções futuras mais fáceis de acompanhar.

Crie um novo arquivo chamado html.js. Esse arquivo será usado mais tarde para retornar texto HTML em uma resposta HTTP. Colocaremos o código do template aqui e o copiaremos para os outros servidores que retornam diversos tipos.

No terminal, digite o seguinte:

touch html.js

Agora abra este arquivo em um editor de texto:

nano html.js

Vamos copiar o “código do modelo.” Digite isso no nano:

const http = require("http");

const host = 'localhost';
const port = 8000;

const requestListener = function (req, res) {};

const server = http.createServer(requestListener);
server.listen(port, host, () => {
    console.log(`Server is running on http://${host}:${port}`);
});

Salve e saia do html.js com CTRL+X, então volte para o terminal.

Agora vamos copiar este arquivo para dois novos arquivos. O primeiro arquivo será usado para retornar dados CSV na resposta HTTP:

cp html.js csv.js

O segundo arquivo retornará uma resposta JSON no servidor:

cp html.js json.js

Os arquivos restantes serão para exercícios posteriores:

cp html.js htmlFile.js

cp html.js routes.js

Agora estamos prontos para continuar nossos exercícios. Vamos começar retornando JSON.

Servindo JSON

JavaScript Object Notation, comumente chamado de JSON, é um formato de intercâmbio de dados baseado em texto. Como o nome sugere, ele é derivado de objetos JavaScript, mas é independente de linguagem, o que significa que pode ser usado por qualquer linguagem de programação que consiga analisar sua sintaxe.

JSON é comumente usado por APIs para aceitar e retornar dados. Sua popularidade se deve ao tamanho menor de transferência de dados em comparação com padrões de troca de dados anteriores, como XML, assim como às ferramentas existentes que permitem que programas os analisem sem esforço excessivo. Se você quiser aprender mais sobre JSON, pode ler nosso guia sobre Como Trabalhar com JSON em JavaScript.

Abra o arquivo json.js com o nano:

nano json.js

Ao servir JSON, você deve definir o cabeçalho Content-Type como application/json. Sem isso, navegadores e clientes de API tratam a resposta como texto simples, fazendo com que fetch().json() falhe. Defina os cabeçalhos antes de chamar writeHead() — uma vez enviados, os cabeçalhos não podem ser modificados.

Erro comum: Chamar res.writeHead(200) antes de res.setHeader() bloqueia os cabeçalhos. Se você tentar definir cabeçalhos após writeHead(), eles serão ignorados. Sempre chame setHeader() primeiro, e depois writeHead().

Modifique a função requestListener():

...
const requestListener = function (req, res) {
    res.setHeader("Content-Type", "application/json");
    res.writeHead(200);
    res.end(`{"message": "This is a JSON response"}`);
};
...

O código de status 200 indica sucesso. O corpo da resposta contém JSON válido.

Por que usar JSON.stringify() em vez de strings manuais? Escrever JSON manualmente é propenso a erros. Se sua mensagem contiver aspas, quebras de linha ou caracteres especiais, você precisará escapá-los manualmente. JSON.stringify() faz isso automaticamente:

const data = { 
    message: "This is a JSON response",
    timestamp: Date.now(),
    note: 'He said "hello"' // Quotes automatically escaped
};
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(JSON.stringify(data));

Cuidado com referências circulares: Se seu objeto fizer referência a si mesmo, JSON.stringify() lançará: TypeError: Converting circular structure to JSON. Isso acontece frequentemente com resultados de consultas ao banco de dados. Sempre valide a estrutura dos seus dados.

O que acontece se você tentar enviar um objeto diretamente? O módulo http só aceita strings ou Buffers. Se você tentar res.end({ message: "test" }), o Node.js lançará: TypeError [ERR_INVALID_ARG_TYPE]: O primeiro argumento deve ser uma string ou Buffer. Você deve converter objetos para strings JSON primeiro.

Verifique se o cabeçalho foi definido corretamente: Use a flag -i com o curl para ver os cabeçalhos da resposta:

curl -i http://localhost:8000

Você deve ver Content-Type: application/json nos cabeçalhos. Se você ver text/plain ou nenhum Content-Type, ou você chamou setHeader() depois de writeHead() ou esqueceu completamente.

Cenário comum de depuração: Se o seu JavaScript frontend receber undefined ao chamar fetch('/api').then(r => r.json()), verifique os cabeçalhos com curl -i. O Content-Type deve ser exatamente application/json (sensível a maiúsculas e minúsculas para alguns analisadores estritos).

Salve e saia do json.js pressionando CTRL+X. Agora, vamos executar o servidor com o comando node:

node json.js

Em outro terminal, vamos acessar o servidor usando cURL:

curl http://localhost:8000

Ao pressionarmos ENTER, veremos o seguinte resultado:

{"message": "This is a JSON response"}

Agora conseguimos retornar com sucesso uma resposta em JSON, assim como muitas das APIs populares com as quais criamos aplicativos. Certifique-se de sair do servidor em execução com CTRL+C para que possamos voltar ao prompt padrão do terminal. Em seguida, vamos dar uma olhada em outro formato popular de retorno de dados: CSV.

Servindo CSV

O formato de arquivo Comma Separated Values (CSV) é um padrão de texto comumente usado para fornecer dados tabulares. Na maioria dos casos, cada linha é separada por uma nova linha e cada item na linha é separado por uma vírgula.

No nosso espaço de trabalho, abra o arquivo csv.js com um editor de texto:

nano csv.js

Vamos adicionar as seguintes linhas à nossa função requestListener():

...
const requestListener = function (req, res) {
    res.setHeader("Content-Type", "text/csv");
    res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
};
...

Desta vez, nosso Content-Type indica que um arquivo CSV está sendo retornado, já que o valor é text/csv. O segundo cabeçalho que adicionamos é Content-Disposition. Este cabeçalho informa ao navegador como exibir os dados, especialmente no navegador ou como um arquivo separado.

Quando retornamos respostas em CSV, a maioria dos navegadores modernos faz o download do arquivo automaticamente, mesmo que o cabeçalho Content-Disposition não esteja definido. No entanto, ao retornar um arquivo CSV, ainda devemos adicionar esse cabeçalho, pois ele permite definir o nome do arquivo CSV. Nesse caso, sinalizamos ao navegador que este arquivo CSV é um anexo e deve ser baixado. Em seguida, informamos ao navegador que o nome do arquivo é oceanpals.csv.

Vamos escrever os dados CSV na resposta HTTP:

...
const requestListener = function (req, res) {
    res.setHeader("Content-Type", "text/csv");
    res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
    res.writeHead(200);
    res.end(`id,name,email\n1,Sammy Shark,shark@ocean.com`);
};
...

Retorne um status 200/OK com os dados CSV. A vírgula separa os valores em cada coluna e o caractere de nova linha (\n) separa as linhas. Temos duas linhas: a linha do cabeçalho e uma linha de dados.

Armadilha na formatação CSV: O exemplo simples acima funciona, mas a geração de CSV em produção requer escape. Se seus dados contêm vírgulas (como "Smith, John") ou aspas, elas quebrarão o formato CSV. O escape correto no CSV envolve colocar os valores entre aspas e duplicar as aspas internas: "Smith, John" se torna "Smith, ""John""" no CSV. Para produção, use uma biblioteca como csv-stringify ou escape os valores corretamente para evitar erros de parsing.

O que acontece sem Content-Disposition? A maioria dos navegadores baixa o arquivo se Content-Type: text/csv estiver definido, mas o nome do arquivo pode ser o caminho da URL ou algo genérico. O cabeçalho attachment;filename=oceanpals.csv garante que o navegador salve com um nome específico. Sem attachment, alguns navegadores tentam exibir o CSV inline, o que geralmente fica bagunçado.

Vamos testar este servidor no navegador. Salve csv.js e saia do editor com CTRL+X.

Execute o servidor com o comando Node.js:

node csv.js

Em outro Terminal, vamos acessar o servidor usando cURL:

curl http://localhost:8000

O console exibirá isto:

id,name,email
1,Sammy Shark,shark@ocean.com

Se formos para http://localhost:8000 em nosso navegador, um arquivo CSV será baixado. O nome do arquivo será oceanpals.csv.

Saia do servidor em execução com CTRL+C para retornar ao prompt padrão do terminal.

Tendo retornado JSON e CSV, cobrimos dois casos que são populares para APIs. Vamos passar para como devolvemos dados para websites que as pessoas acessam em um navegador.

Servindo HTML

HTML (HyperText Markup Language) é o formato mais comum a ser usado quando queremos que os usuários interajam com nosso servidor através de um navegador web. Ele foi criado para estruturar o conteúdo da web. Os navegadores web são feitos para exibir conteúdo HTML, assim como qualquer estilo que adicionarmos com CSS, outra tecnologia de front-end que nos permite alterar a estética dos nossos sites.

Vamos reabrir html.js com nosso editor de texto:

nano html.js

Modifique a função requestListener() para retornar o cabeçalho Content-Type apropriado para uma resposta HTML:

...
const requestListener = function (req, res) {
    res.setHeader("Content-Type", "text/html");
};
...

Agora, vamos retornar o conteúdo HTML para o usuário. Adicione as linhas destacadas em html.js para que fique assim:

...
const requestListener = function (req, res) {
    res.setHeader("Content-Type", "text/html");
    res.writeHead(200);
    res.end(`<html><body><h1>This is HTML</h1></body></html>`);
};
...

Primeiro, adicionamos o código de status HTTP. Em seguida, chamamos response.end() com um argumento de string que contém HTML válido. Quando acessamos nosso servidor no navegador, veremos uma página HTML com uma tag de cabeçalho contendo Isso é HTML.

Vamos salvar e sair pressionando CTRL+X. Agora, vamos executar o servidor com o comando node:

node html.js

Veremos Servidor está rodando em http://localhost:8000 quando nosso programa tiver iniciado.

Agora vá para o navegador e visite http://localhost:8000. Nossa página ficará assim:

Vamos encerrar o servidor em execução com CTRL+C e voltar ao prompt padrão do terminal.

É comum que o HTML seja escrito em um arquivo, separado do código do lado do servidor, como nossos programas em Node.js. Em seguida, vamos ver como podemos retornar respostas HTML a partir de arquivos.

Etapa 3 — Servindo uma Página HTML a Partir de um Arquivo

Podemos servir HTML como strings no Node.js para o usuário, mas é preferível que carreguemos arquivos HTML e sirvamos seu conteúdo. Dessa forma, à medida que o arquivo HTML cresce, não precisamos manter strings longas em nosso código Node.js, mantendo-o mais conciso e permitindo que trabalhemos em cada aspecto do nosso site de forma independente. Essa "separação de responsabilidades" é comum em muitas configurações de desenvolvimento web, então é bom saber como carregar arquivos HTML para suportá-la no Node.js.

Para servir arquivos HTML, carregamos o arquivo HTML com o módulo fs e usamos seus dados ao escrever nossa resposta HTTP.

Primeiro, vamos criar um arquivo HTML que o servidor web irá retornar. Crie um novo arquivo HTML:

touch index.html

Agora abra index.html em um editor de texto:

nano index.html

Nossa página da web será minimalista. Ela terá um fundo laranja e exibirá um texto de saudação no centro. Adicione este código ao arquivo:

<!DOCTYPE html>

<head>
    <title>My Website</title>
    <style>
        *,
        html {
            margin: 0;
            padding: 0;
            border: 0;
        }

        html {
            width: 100%;
            height: 100%;
        }

        body {
            width: 100%;
            height: 100%;
            position: relative;
            background-color: rgb(236, 152, 42);
        }

        .center {
            width: 100%;
            height: 50%;
            margin: 0;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: white;
            font-family: "Trebuchet MS", Helvetica, sans-serif;
            text-align: center;
        }

        h1 {
            font-size: 144px;
        }

        p {
            font-size: 64px;
        }
    </style>
</head>

<body>
    <div class="center">
        <h1>Hello Again!</h1>
        <p>This is served from a file</p>
    </div>
</body>

</html>

Esta única página da web mostra duas linhas de texto: Hello Again! e This is served from a file. As linhas aparecem no centro da página, uma acima da outra. A primeira linha de texto é exibida como um cabeçalho, o que significa que será grande. A segunda linha de texto aparecerá um pouco menor. Todo o texto aparecerá em branco e a página da web terá um fundo laranja.

Embora não seja o foco deste artigo ou série, se você estiver interessado em aprender mais sobre HTML, CSS e outras tecnologias de front-end para a web, você pode dar uma olhada no guia 'Começando na Web' da Mozilla.

Isso é tudo que precisamos para o HTML, então salve e saia do arquivo com CTRL+X. Agora podemos passar para o código do servidor.

Para este exercício, vamos trabalhar no htmlFile.js. Abra-o com o editor de texto:

nano htmlFile.js

Como precisamos ler um arquivo, vamos começar importando o módulo fs:

const http = require("http");
const fs = require('fs').promises;
...

Este módulo contém uma função readFile() que usaremos para carregar o arquivo HTML no local. Importamos a variante baseada em promessa seguindo as melhores práticas modernas de JavaScript. Usamos promessas, pois elas são sintaticamente mais concisas do que callbacks, que teríamos que usar se atribuíssemos fs apenas com require('fs'). Para saber mais sobre melhores práticas de programação assíncrona, você pode ler nosso guia Como Escrever Código Assíncrono em Node.js.

Queremos que nosso arquivo HTML seja lido quando um usuário solicitar nosso sistema. Vamos começar modificando requestListener() para ler o arquivo:

...
const requestListener = function (req, res) {
    fs.readFile(__dirname + "/index.html")
};
...

Usamos o método fs.readFile() para carregar o arquivo. Seu argumento é __dirname + "/index.html". A variável especial __dirname tem o caminho absoluto de onde o código Node.js está sendo executado. Em seguida, adicionamos /index.html para podermos carregar o arquivo HTML que criamos anteriormente.

Agora vamos retornar a página HTML assim que ela for carregada:

...
const requestListener = function (req, res) {
    fs.readFile(__dirname + "/index.html")
        .then(contents => {
            res.setHeader("Content-Type", "text/html");
            res.writeHead(200);
            res.end(contents);
        })
};
...

Se a promessa fs.readFile() for resolvida com sucesso, ela retornará seus dados. Usamos o método then() para lidar com esse caso. O parâmetro contents contém os dados do arquivo HTML.

Primeiro, definimos o cabeçalho Content-Type como text/html para informar ao cliente que estamos retornando dados HTML. Em seguida, escrevemos o código de status para indicar que a requisição foi bem-sucedida. Por fim, enviamos ao cliente a página HTML que carregamos, com os dados na variável contents.

O método fs.readFile() pode falhar às vezes, então devemos tratar esse caso quando ocorrer um erro. Adicione isso à função requestListener():

...
const requestListener = function (req, res) {
    fs.readFile(__dirname + "/index.html")
        .then(contents => {
            res.setHeader("Content-Type", "text/html");
            res.writeHead(200);
            res.end(contents);
        })
        .catch(err => {
            res.writeHead(500);
            res.end('Internal Server Error');
            return;
        });
};
...

Salve o arquivo e saia do nano com CTRL+X.

Quando uma promessa encontra um erro, ela é rejeitada. Nós lidamos com esse caso usando o método catch(). Ele define o código de status como 500, indicando que um erro interno foi encontrado, e envia uma mensagem de erro amigável ao usuário. Em produção, você nunca deve expor os detalhes crus do erro aos usuários, pois isso pode vazar informações sensíveis.

Execute nosso servidor com o comando node:

node htmlFile.js

No navegador da web, visite http://localhost:8000. Você verá esta página:

Você agora retornou uma página HTML do servidor para o usuário. Você pode encerrar o servidor em execução com CTRL+C. Você verá o prompt do terminal retornar quando fizer isso.

Ao escrever código assim em produção, você pode não querer carregar uma página HTML toda vez que receber uma solicitação HTTP. Embora esta página HTML tenha cerca de 800 bytes, sites mais complexos podem ter megabytes de tamanho. Arquivos grandes podem levar algum tempo para carregar. Se o seu site estiver esperando muito tráfego, pode ser melhor carregar os arquivos HTML na inicialização e salvar seu conteúdo. Depois de carregados, você pode configurar o servidor e fazê-lo escutar solicitações em um endereço.

Para demonstrar esse método, vamos ver como podemos reformular nosso servidor para torná-lo mais eficiente e escalável.

Servindo HTML de Forma Eficiente

Em vez de carregar o HTML a cada solicitação, nesta etapa vamos carregá-lo uma vez no início. A solicitação retornará os dados que carregamos na inicialização.

No terminal, reabra o script do Node.js com um editor de texto:

nano htmlFile.js

Vamos começar adicionando uma nova variável antes de criarmos a função requestListener():

...
let indexFile;

const requestListener = function (req, res) {
...

Quando executamos este programa, esta variável irá conter o conteúdo do arquivo HTML.

Agora, vamos reajustar a função requestListener(). Em vez de carregar o arquivo, ela agora retornará o conteúdo de indexFile:

...
const requestListener = function (req, res) {
    res.setHeader("Content-Type", "text/html");
    res.writeHead(200);
    res.end(indexFile);
};
...

Em seguida, movemos a lógica de leitura de arquivos da função requestListener() para a inicialização do nosso servidor. Faça as seguintes alterações enquanto criamos o servidor:

...

const server = http.createServer(requestListener);

fs.readFile(__dirname + "/index.html")
    .then(contents => {
        indexFile = contents;
        server.listen(port, host, () => {
            console.log(`Server is running on http://${host}:${port}`);
        });
    })
    .catch(err => {
        console.error(`Could not read index.html file: ${err}`);
        process.exit(1);
    });

Salve o arquivo e saia do nano com CTRL+X.

O código que lê o arquivo é semelhante ao que escrevemos em nossa primeira tentativa. No entanto, quando conseguimos ler o arquivo, agora salvamos o conteúdo em nossa variável global indexFile. Em seguida, iniciamos o servidor com o método listen(). O ponto principal é que o arquivo é carregado antes do servidor ser executado. Dessa forma, a função requestListener() terá certeza de retornar uma página HTML, já que indexFile não é mais uma variável vazia.

Nosso manipulador de erros também mudou. Se o arquivo não puder ser carregado, capturamos o erro e o imprimimos no nosso console. Em seguida, saímos do programa Node.js com a função exit() sem iniciar o servidor. Dessa forma, podemos ver por que a leitura do arquivo falhou, resolver o problema e então iniciar o servidor novamente.

Agora criamos diferentes servidores web que retornam vários tipos de dados para um usuário. Até agora, não usamos nenhum dado da requisição para determinar o que deve ser retornado. Precisaremos usar dados da requisição ao configurar diferentes rotas ou caminhos em um servidor Node.js, então, a seguir, vamos ver como eles funcionam juntos.

Etapa 4 — Gerenciando Rotas Usando um Objeto de Requisição HTTP

A maioria dos sites que visitamos ou APIs que usamos geralmente tem mais de um endpoint para que possamos acessar vários recursos. Um bom exemplo seria um sistema de gerenciamento de livros, algo que poderia ser usado em uma biblioteca. Ele não precisaria apenas gerenciar os dados dos livros, mas também gerenciaria os dados dos autores para facilitar o catálogo e a busca.

Embora os dados de livros e autores estejam relacionados, eles são dois objetos diferentes. Nesses casos, os desenvolvedores de software geralmente codificam cada objeto em endpoints diferentes como uma forma de indicar ao usuário da API com que tipo de dados ele está interagindo.

Vamos criar um novo servidor para uma pequena biblioteca, que retornará dois tipos diferentes de dados. Se o usuário acessar o endereço do nosso servidor em /books, ele receberá uma lista de livros em JSON. Se acessar /authors, ele receberá uma lista de informações sobre os autores em JSON.

Até agora, o servidor retorna a mesma resposta para todas as solicitações. Para suportar múltiplos recursos, armazene seus dados em variáveis e use roteamento condicional. Por exemplo:

const books = JSON.stringify([
  { title: "The Alchemist", author: "Paulo Coelho", year: 1988 },
  { title: "The Prophet", author: "Kahlil Gibran", year: 1923 }
]);
const authors = JSON.stringify([
  { name: "Paulo Coelho", countryOfBirth: "Brazil", yearOfBirth: 1947 },
  { name: "Kahlil Gibran", countryOfBirth: "Lebanon", yearOfBirth: 1883 }
]);

const requestListener = function (req, res) {
  res.setHeader("Content-Type", "application/json");
  switch (req.url) {
    case "/books":
      res.writeHead(200);
      res.end(books);
      break;
    case "/authors":
      res.writeHead(200);
      res.end(authors);
      break;
    default:
      res.writeHead(404);
      res.end(JSON.stringify({error:"Resource not found"}));
  }
}

Teste com curl http://localhost:8000/books ou /authors para ver os dados respectivos. Qualquer outro URL retorna um erro 404.

O problema com a correspondência simples de strings: req.url é uma string bruta que inclui tudo após o domínio—caminhos, strings de consulta e fragmentos. Se alguém visitar /books?id=123, seu case "/books" não corresponderá porque req.url é /books?id=123, não /books. Parâmetros de consulta quebram o roteamento: /books?sort=name não corresponderá a /books. Caracteres codificados em URL podem quebrar o roteamento: /books%20reviews (espaço codificado como %20) não corresponderá a /books reviews.

Use a API URL em vez disso: A API URL separa automaticamente o caminho dos parâmetros de consulta, decodifica caracteres codificados em URL e lida corretamente com casos extremos.

const host = req.headers.host || 'localhost:8000';
const url = new URL(req.url, `http://${host}`);

switch (url.pathname) {
  case '/books':
    // Validate HTTP method - only GET allowed for this endpoint
    if (req.method !== 'GET') {
      res.writeHead(405, { 'Allow': 'GET' });
      return res.end(JSON.stringify({ error: 'Method not allowed' }));
    }
    res.writeHead(200);
    return res.end(books);

  case '/authors':
    if (req.method !== 'GET') {
      res.writeHead(405, { 'Allow': 'GET' });
      return res.end(JSON.stringify({ error: 'Method not allowed' }));
    }
    res.writeHead(200);
    return res.end(authors);

  default:
    res.writeHead(404);
    return res.end(JSON.stringify({ error: 'Resource not found' }));
}

Por que verificar o método HTTP? O mesmo caminho pode suportar diferentes métodos HTTP (GET para recuperar, POST para criar). Sem essa verificação, uma requisição POST para /books retornaria a lista de livros em vez de criar um novo livro, o que quebra as convenções de APIs REST. O código de status 405 Method Not Allowed com o cabeçalho Allow informa aos clientes quais métodos são válidos para este endpoint.

Manipulação de query string: A API URL separa automaticamente o caminho (/books) dos parâmetros de consulta (?id=123), então /books?id=123 direciona corretamente para o endpoint de livros, enquanto você ainda pode acessar url.searchParams.get('id') se necessário.

Teste vários cenários:

curl http://localhost:8000/books

curl http://localhost:8000/authors  

curl http://localhost:8000/books?id=123  # Query string doesn't break routing

curl -X POST http://localhost:8000/books  # Returns 405 Method Not Allowed

curl http://localhost:8000/nonexistent  # Returns 404

Passo 5 — Lidando com requisições POST e analisando corpos de requisição

Até agora, só lidamos com requisições GET. A maioria das APIs precisa aceitar dados dos clientes, o que normalmente acontece através de requisições POST. O módulo http exige a análise manual do corpo das requisições, que são recebidas como fluxos.

Insight crítico: Ao contrário das solicitações GET, nas quais os dados vêm na URL, os corpos das solicitações POST chegam como um fluxo. Isso significa que os dados não chegam de uma vez — eles chegam em pedaços. Se você tentar analisar o JSON antes de todos os pedaços chegarem, obterá dados incompletos ou com formato incorreto. Você deve coletar todos os pedaços primeiro e só então analisar.

Veja o que acontece se você tentar analisar cedo demais:

// WRONG - Body isn't complete yet
req.on('data', chunk => {
  const data = JSON.parse(chunk); // Will fail - chunk is only part of the body
});

// CORRECT - Collect all chunks first
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
  const data = JSON.parse(body); // Now body is complete
});

Crie um servidor que aceite requisições POST com payloads JSON. Crie um novo arquivo:

touch post.js

cp routes.js post.js

Abra post.js:

nano post.js

Aqui está um analisador de corpo JSON pronto para produção que lida com questões de segurança:

const MAX_BODY = 1 * 1024 * 1024; // 1MB limit prevents memory exhaustion

function parseJSONBody(req, res) {
  return new Promise((resolve, reject) => {
    let body = '';
    let received = 0;
    
    // Validate Content-Type before processing
    const contentType = req.headers['content-type'] || '';
    if (!contentType.startsWith('application/json')) {
      res.writeHead(415, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Unsupported Content-Type' }));
      return reject(new Error('Unsupported Content-Type'));
    }
    
    // Collect data chunks
    req.on('data', chunk => {
      received += chunk.length;
      
      // Prevent memory exhaustion from huge payloads
      if (received > MAX_BODY) {
        res.writeHead(413, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Payload too large' }));
        req.destroy(); // Stop receiving more data
        return reject(new Error('Payload too large'));
      }
      body += chunk.toString(); // Convert Buffer to string
    });
    
    // All chunks received, now parse
    req.on('end', () => {
      try {
        if (!body) {
          res.writeHead(400, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify({ error: 'Empty request body' }));
          return reject(new Error('Empty request body'));
        }
        
        const data = JSON.parse(body);
        
        // Validate it's an object (not a string, number, etc.)
        if (typeof data !== 'object' || data === null || Array.isArray(data)) {
          res.writeHead(400, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify({ error: 'Invalid JSON object' }));
          return reject(new Error('Invalid JSON object'));
        }
        
        resolve(data);
      } catch (parseError) {
        // JSON.parse() throws for malformed JSON
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Malformed JSON' }));
        console.error('JSON parse error:', parseError.message);
      }
    });
    
    // Handle network errors
    req.on('error', (error) => {
      console.error('Request error:', error.message);
      res.writeHead(400, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Request error' }));
    });
  });
}

Por que verificar o Content-Type? Se um cliente enviar dados de formulário com Content-Type: application/x-www-form-urlencoded, tentar analisá-los como JSON falhará. O código de status 415 Unsupported Media Type informa ao cliente que o Content-Type dele não é aceitável.

Por que limitar o tamanho do corpo? Sem um limite, um cliente malicioso poderia enviar gigabytes de dados, esgotando a memória do seu servidor. A resposta 413 Payload Too Large e o req.destroy() interrompem a conexão imediatamente quando o limite é excedido.

Por que validar o resultado analisado? JSON.parse('"hello"') funciona, mas retorna uma string, não um objeto. Seu código pode esperar que data.property funcione, mas strings não têm propriedades. A verificação garante que você obtenha um objeto real.

Use esta função no seu manipulador POST:

const requestListener = async function (req, res) {
  if (req.method === 'POST' && req.url === '/books') {
    const bookData = await parseJSONBody(req, res);
    // Now use bookData safely
    // ...
  }
};

Etapa 6 — Servindo Arquivos Estáticos

Muitas aplicações web precisam fornecer recursos estáticos, como imagens, arquivos CSS, arquivos JavaScript ou outros recursos. Embora o módulo http não tenha suporte nativo para servir arquivos estáticos como o express.static do Express, você pode implementá-lo usando os módulos fs e path.

Problema crítico de segurança: Se você usar de forma ingênua path.join(__dirname, 'public', req.url) sem validação, um invasor pode acessar qualquer arquivo no seu servidor usando ataques de travessia de diretórios. Uma requisição como GET /../../../etc/passwd poderia expor arquivos do sistema. A verificação com path.resolve() garante que o caminho resolvido nunca saia do diretório public.

Aqui está o que pode dar errado sem uma validação adequada do caminho:

// VULNERABLE - Allows directory traversal
const filePath = path.join(__dirname, 'public', req.url);
// Request: /../../../etc/passwd
// Resolves to: /etc/passwd (outside public directory!)

// SECURE - Validates path stays within public directory
const safePath = path.resolve(publicDir, '.' + reqPath);
if (!safePath.startsWith(publicDir)) {
  res.writeHead(403);
  res.end('Forbidden');
  return;
}
// Request: /../../../etc/passwd
// safePath: /home/user/app/public/../../../etc/passwd
// Resolved: /etc/passwd
// Check fails because /etc/passwd doesn't start with /home/user/app/public

Crie um servidor de arquivos estáticos seguro:

touch static.js

Abra static.js:

nano static.js

Sirva arquivos estáticos de forma eficiente e segura transmitindo-os com fs.createReadStream() e resolução de caminho segura:

const http = require("http");
const fs = require("fs");
const path = require("path");

const host = 'localhost';
const port = 8000;

const mimeTypes = {
  '.html': 'text/html',
  '.css': 'text/css',
  '.js': 'application/javascript',
  '.json': 'application/json',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg',
  '.gif': 'image/gif',
  '.svg': 'image/svg+xml',
  '.ico': 'image/x-icon',
  '.pdf': 'application/pdf'
};

const publicDir = path.resolve(__dirname, 'public');

const requestListener = (req, res) => {
  // Normalize the request path
  let reqPath = req.url === '/' ? '/index.html' : req.url;
  
  // Strip query string - ?id=123 shouldn't affect file path
  reqPath = reqPath.split('?')[0];
  
  // Resolve path and check it stays within publicDir
  // The '.' prefix prevents absolute paths from being resolved incorrectly
  const safePath = path.resolve(publicDir, '.' + reqPath);
  
  // Critical security check: ensure resolved path is within publicDir
  if (!safePath.startsWith(publicDir)) {
    res.writeHead(403, { 'X-Content-Type-Options': 'nosniff' });
    res.end('Forbidden');
    return;
  }
  
  // Check if file exists and is actually a file (not a directory)
  fs.stat(safePath, (err, stat) => {
    if (err || !stat.isFile()) {
      res.writeHead(404, { 'X-Content-Type-Options': 'nosniff' });
      res.end('File not found');
      return;
    }
    
    // Determine MIME type from file extension
    const ext = path.extname(safePath).toLowerCase();
    const contentType = mimeTypes[ext] || 'application/octet-stream';
    
    res.setHeader("Content-Type", contentType);
    res.setHeader("X-Content-Type-Options", "nosniff"); // Prevents MIME sniffing attacks
    res.writeHead(200);
    
    // Stream file instead of loading into memory
    // Critical for large files - won't consume all memory
    const stream = fs.createReadStream(safePath);
    stream.pipe(res);
    
    stream.on('error', () => {
      // File might be deleted between stat() and createReadStream()
      res.writeHead(500, { 'X-Content-Type-Options': 'nosniff' });
      res.end('Server error');
    });
  });
};

const server = http.createServer(requestListener);
server.listen(port, host, () => {
  console.log(`Server is running on http://${host}:${port}`);
  console.log(`Serving static files from: ${publicDir}`);
});

Crie um diretório public e adicione um arquivo de teste:

mkdir public

echo "<h1>Static File Served!</h1>" > public/index.html

Execute o servidor:

node static.js

Visite http://localhost:8000 no seu navegador para ver o arquivo estático servido.

Por que usar streaming em vez de fs.readFile()? Para um arquivo de vídeo de 100 MB, o fs.readFile() carrega todo o arquivo na memória antes de enviá-lo. Com o fs.createReadStream(), os dados fluem diretamente do disco para a rede em pequenos pedaços, usando memória mínima independentemente do tamanho do arquivo.

Por que verificar stat.isFile()? Se alguém solicitar um caminho de diretório como /images/, fs.stat() funcionará, mas isFile() retornará falso. Sem essa verificação, você tentaria transmitir um diretório, o que falharia. A verificação garante que você só sirva arquivos reais.

Teste a proteção contra travessia de caminho:

curl http://localhost:8000/../../../etc/passwd

# Should return 403 Forbidden, not the system file

Passo 7 — Tratamento de Erros e Registro

O tratamento robusto de erros é crucial para servidores de produção. O módulo http não fornece tratamento automático de erros, então você deve implementá-lo por conta própria.

O tratamento adequado de erros envolve capturar erros assíncronos, registrá-los de forma apropriada, enviar respostas de erro amigáveis ao usuário e evitar falhas no servidor devido a erros não tratados. Vamos aprimorar nosso servidor com um tratamento de erros e registro abrangentes:

const http = require("http");
const fs = require('fs').promises;

const host = 'localhost';
const port = 8000;

// Simple logging utility
const log = {
    info: (msg) => console.log(`[INFO] ${new Date().toISOString()} - ${msg}`),
    error: (msg, err) => console.error(`[ERROR] ${new Date().toISOString()} - ${msg}`, err),
    warn: (msg) => console.warn(`[WARN] ${new Date().toISOString()} - ${msg}`)
};

const requestListener = async function (req, res) {
    try {
        log.info(`${req.method} ${req.url} - ${req.headers['user-agent'] || 'Unknown'}`);
        if (req.url === '/') {
            try {
                const data = await fs.readFile(__dirname + '/index.html');
                res.setHeader("Content-Type", "text/html");
                res.writeHead(200);
                res.end(data);
            } catch (error) {
                log.error('Failed to read index.html', error);
                res.writeHead(500);
                res.end('<h1>Internal Server Error</h1><p>Unable to load page.</p>');
            }
        } else if (req.url === '/error') {
            throw new Error('Simulated error for testing');
        } else {
            res.writeHead(404);
            res.end('<h1>Not Found</h1><p>The requested resource was not found.</p>');
        }
    } catch (error) {
        log.error('Unhandled error in request handler', error);
        const errorMessage = process.env.NODE_ENV !== 'production'
            ? error.message
            : 'An unexpected error occurred';
        res.writeHead(500);
        res.end(`<h1>Internal Server Error</h1><p>${errorMessage}</p>`);
    }
};

const server = http.createServer(requestListener);

// Handle server-level errors
server.on('error', (error) => {
    if (error.code === 'EADDRINUSE') {
        log.error(`Port ${port} is already in use`);
    } else {
        log.error('Server error', error);
    }
    process.exit(1);
});

// Handle client connection errors
server.on('clientError', (err, socket) => {
    log.warn('Client error: ' + err.message);
    socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});

server.listen(port, host, () => {
    log.info(`Server is running on http://${host}:${port}`);
});

Encerramento Elegante (SIGTERM/SIGINT)

Ao executar sob gerenciadores de processos como PM2 ou Docker, trate os sinais de término para fechar o servidor de maneira adequada:

const connections = new Set();

server.on('connection', socket => {
  connections.add(socket);
  socket.on('close', () => connections.delete(socket));
});

const shutdown = (signal) => {
  log.warn(`${signal} received: shutting down gracefully...`);
  server.close(() => {
    log.info('Server closed.');
    process.exit(0);
  });
  setTimeout(() => {
    log.warn('Forcing socket close...');
    connections.forEach(s => s.destroy());
    process.exit(1);
  }, 10000).unref();
};

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

Principais práticas de manuseio de erros mostradas:

  1. Blocos try-catch em torno de operações assíncronas
  2. Manipuladores de erro no nível do servidor para problemas de conexão e porta
  3. Tratamento de erros do cliente para solicitações malformadas
  4. Registro com carimbos de data/hora e níveis de severidade
  5. Mensagens de erro conscientes do ambiente (não exponha rastros de pilha em produção)
  6. Respostas de erro elegantes com códigos de status HTTP apropriados

Melhor Prática: Em produção, considere usar bibliotecas de logging estruturado como Winston ou Pino, e envie erros críticos para serviços de monitoramento de erros como Sentry ou Rollbar. Este exemplo usa logging simples no console para fins educativos.

Passo 8 — Usando a Sintaxe de Módulos ES (ESM)

O Node.js moderno suporta Módulos ES (ESM) usando a sintaxe import/export em vez do CommonJS require/module.exports. Para usar ESM, é preciso nomear seu arquivo com a extensão .mjs ou definir "type": "module" no seu package.json.

A sintaxe ESM usa import em vez de require e export em vez de module.exports. Aqui está o mesmo servidor básico usando a sintaxe de Módulo ES:

Crie server.mjs:

touch server.mjs

nano server.mjs
import http from 'http';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';

// ES Modules don't have __dirname, so we recreate it
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const host = 'localhost';
const port = 8000;

const requestListener = async (req, res) => {
    try {
        if (req.url === '/') {
            const data = await fs.readFile(path.join(__dirname, 'index.html'));
            res.setHeader("Content-Type", "text/html");
            res.writeHead(200);
            res.end(data);
        } else {
            res.writeHead(404);
            res.end('Not found');
        }
    } catch (error) {
        console.error('Error:', error);
        res.writeHead(500);
        res.end('Server error');
    }
};

const server = http.createServer(requestListener);

server.listen(port, host, () => {
    console.log(`Server is running on http://${host}:${port}`);
});

Execute:

node server.mjs

Principais Diferenças: No ESM, __dirname não está disponível — você precisa derivá-lo de import.meta.url. Além disso, caminhos de arquivos em operações com fs podem precisar de resolução explícita. Fora isso, a API do módulo HTTP permanece idêntica.

Como alternativa, crie package.json com "type": "module":

{
  "type": "module"
}

Então, use arquivos .js com a sintaxe ESM:

import http from 'http';

const server = http.createServer((req, res) => {
    res.writeHead(200);
    res.end('ESM Server');
});

server.listen(8000);

Considerações de Produção

Ao passar do desenvolvimento para a produção, várias considerações importantes entram em jogo:

Suporte HTTPS

O módulo http serve apenas HTTP. Para HTTPS, use o módulo https da mesma forma:

const https = require('https');
const fs = require('fs');

const options = {
    key: fs.readFileSync('path/to/private-key.pem'),
    cert: fs.readFileSync('path/to/certificate.pem')
};

const server = https.createServer(options, requestListener);

Segurança: Nunca comprometa chaves privadas ou certificados no controle de versão. Use variáveis de ambiente ou serviços seguros de gerenciamento de segredos em produção.

Configuração de Proxy Reverso

Em produção, servidores Node.js normalmente funcionam atrás de proxies reversos como Nginx ou Apache. O proxy lida com a terminação SSL, arquivos estáticos e balanceamento de carga. Seu servidor Node.js deve:

  • Escute apenas em localhost ou 127.0.0.1
  • Leia os cabeçalhos X-Forwarded-For para obter os IPs dos clientes
  • Confie nos cabeçalhos do proxy apenas do seu servidor proxy

Veja nosso guia sobre Como Configurar o Nginx como um Proxy Reverso no Ubuntu para detalhes de configuração.

Gestão de Processos

Para produção, use gerenciadores de processos como o PM2 para lidar com:

  • Reinícios automáticos em falhas
  • Implantações sem tempo de inatividade
  • Gestão de logs
  • Modo em cluster para utilizar múltiplos núcleos de CPU

Saiba mais no nosso guia Como Configurar uma Aplicação Node.js para Produção no Ubuntu.

Otimização de Desempenho

  • Cache de arquivos: Carregue arquivos estáticos na inicialização (como mostrado no Passo 3) em vez de ler do disco a cada solicitação. Isso reduz significativamente a sobrecarga de I/O.

  • Transmita arquivos grandes: Para arquivos com mais de 1MB, use fs.createReadStream() e direcione para a resposta em vez de carregar o arquivo inteiro na memória.

  • Pooling de conexões: Para conexões de banco de dados, use pooling de conexões para evitar o overhead de criar novas conexões a cada requisição.

  • Compressão: Use o módulo zlib para compressão gzip em respostas de texto para reduzir a largura de banda:

    const zlib = require('zlib');
    const gzip = zlib.createGzip();
    res.setHeader('Content-Encoding', 'gzip');
    gzip.pipe(res);
    gzip.write(data);
    gzip.end();
  • Modo cluster: Use o módulo cluster do Node.js para criar processos de trabalho equivalentes ao número de núcleos da CPU, maximizando o desempenho em operações que exigem muito da CPU.

  • Tempo limite de requisição: Defina valores de tempo limite razoáveis para evitar que clientes lentos consumam recursos:

    server.setTimeout(30000); // 30 seconds
  • Monitoramento de memória: Acompanhe o uso de memória para detectar vazamentos cedo. Em produção, monitore o tamanho do heap e implemente reinícios suaves se a memória crescer sem limites.

Melhores Práticas de Cache e Compressão

Para melhorar ainda mais o desempenho e reduzir os tempos de resposta, aplique cache e compressão onde for apropriado.

  • Ativar cache para arquivos estáticos:
    Adicione os cabeçalhos Cache-Control e ETag às respostas dos seus arquivos estáticos. Por exemplo:

    const stat = fs.statSync(safePath);
    res.setHeader('Cache-Control', 'public, max-age=86400'); // cache for 1 day
    res.setHeader('ETag', `"${stat.size}-${stat.mtime.getTime()}"`);

    Navegadores e CDNs reutilizarão cópias em cache até que o ETag ou a duração do cache expire.

  • Use compressão gzip ou Brotli:
    Respostas baseadas em texto, como HTML, CSS, JS e JSON, se comprimem muito bem. Você pode aplicar compressão com o módulo zlib do Node:

    const zlib = require('zlib');
    const stream = fs.createReadStream(safePath);
    res.setHeader('Content-Encoding', 'gzip');
    stream.pipe(zlib.createGzip()).pipe(res);

    Isso reduz o uso de largura de banda e acelera a entrega para clientes com conexões mais lentas.

  • Evite armazenar em cache excessivamente endpoints dinâmicos:
    Para APIs ou dados frequentemente atualizados, defina durações de cache mais curtas (por exemplo, max-age=60) ou use no-store quando necessário.

Essas otimizações complementam o streaming de arquivos estáticos e o desligamento suave, ajudando seu servidor Node.js a oferecer alto desempenho enquanto permanece seguro e previsível.

Use o módulo http do Node.js para APIs minimalistas, roteamento personalizado ou para aprender sobre o funcionamento interno do HTTP.
Use Express/Fastify quando precisar de middleware, templates ou prototipagem rápida.

Solução de Problemas Comuns

Porta Já em Uso (EADDRINUSE)

Erro: Erro: listen EADDRINUSE: endereço já está em uso :::8000

Solução:

  • Encontre o processo usando a porta: lsof -i :8000 (macOS/Linux) ou netstat -ano | findstr :8000 (Windows)
  • Mate o processo ou use uma porta diferente
  • Adicione tratamento de erros (como mostrado no Passo 7) para lidar com isso de forma adequada

Corpo da solicitação não recebido

Problema: O corpo da requisição POST está vazio ou indefinido

Solução: Corpos de requisição são fluxos—você deve coletar os blocos. Sempre use o padrão mostrado no Passo 5:

let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
    // Now parse body
});

Erros de CORS no Navegador

Problema: Navegador bloqueia solicitações devido à política de CORS

Solução: Adicione cabeçalhos CORS na sua resposta:

res.setHeader("Access-Control-Allow-Origin", "*"); // Or specific domain
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");

Segurança: Usar "*" para Access-Control-Allow-Origin permite qualquer origem. Em produção, especifique explicitamente o domínio do seu frontend.

Arquivos Estáticos Retornam 404

Problema: Arquivos estáticos não são encontrados, mesmo existindo

Solução:

  • Verifique a resolução do caminho do arquivo
  • Verifique se __dirname aponta para o diretório correto
  • Certifique-se de que as permissões do arquivo permitem leitura
  • Verifique se a sanitização de caminhos não está removendo caracteres válidos

Perguntas Frequentes

O que é o módulo HTTP no Node.js?

O módulo http é o módulo nativo do Node.js para criar servidores e clientes HTTP. Ele fornece APIs de baixo nível para lidar com requisições e respostas HTTP sem exigir dependências externas. Faz parte do núcleo do Node.js, portanto, está disponível em toda instalação do Node.js.

Como eu crio um servidor web simples em Node.js?

Crie um servidor usando http.createServer() com uma função de escuta de requisições:

const http = require('http');

const server = http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello World');
});

server.listen(8000);

Isso cria um servidor ouvindo na porta 8000 que responde a todas as solicitações com "Hello World".

Qual é a diferença entre o módulo HTTP e o Express.js?

O módulo http é uma funcionalidade central do Node.js que fornece tratamento de HTTP em baixo nível. O Express.js é um framework construído sobre o http que adiciona roteamento, middleware, modelos de template e outras conveniências. O módulo http oferece controle total, mas requer configuração manual, enquanto o Express.js fornece abstrações à custa de dependências adicionais.

Use o módulo http quando:

  • Construindo APIs ou microsserviços leves
  • Aprendendo os fundamentos do HTTP
  • Você quer zero dependências
  • Controle total sobre o gerenciamento de solicitações/respostas

Use Express.js quando:

  • Construindo aplicativos complexos com muitas rotas
  • Você precisa de um ecossistema de middleware
  • Protótipo rápido é uma prioridade
  • Você quer padrões padrão da comunidade

Para uma introdução abrangente ao Express.js e como ele simplifica o desenvolvimento web, veja nosso guia sobre os Conceitos Básicos do Express no Node.js.

Posso criar uma API REST usando Node.js sem o Express?

Sim, absolutamente. O módulo http é suficiente para construir APIs REST. Você precisará lidar manualmente com:

  • Análise de rotas (usando req.url e req.method)
  • Análise do corpo da requisição (coletando fragmentos do fluxo)
  • Formatação de resposta
  • Tratamento de erros

Para APIs simples, isso geralmente é preferível a adicionar dependências de framework. Veja os Passos 4 e 5 deste tutorial para exemplos de roteamento e tratamento de requisições POST.

Como eu lido com diferentes rotas usando o módulo HTTP do Node.js?

Analise req.url e use lógica condicional ou uma instrução switch:

const requestListener = (req, res) => {
    switch (req.url) {
        case '/':
            res.writeHead(200);
            res.end('Home page');
            break;
        case '/about':
            res.writeHead(200);
            res.end('About page');
            break;
        default:
            res.writeHead(404);
            res.end('Not found');
    }
};

Para roteamento mais complexo, você pode analisar caminhos de URL, strings de consulta usando o módulo url, e lidar com diferentes métodos HTTP.

Como envio respostas em HTML ou JSON com o módulo HTTP?

Defina corretamente o cabeçalho Content-Type e envie os dados:

HTML:

res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end('<h1>Hello</h1>');

JSON:

res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(JSON.stringify({ message: 'Hello' }));

O cabeçalho Content-Type informa ao cliente como interpretar os dados da resposta.

Qual porta devo usar para um servidor Node.js local?

Portas comuns de desenvolvimento são:

  • 8000 ou 8080: Desenvolvimento HTTP geral
  • 3000: Popular com frameworks como o Express
  • 5000: Também comum

Qualquer porta acima de 1024 funciona para usuários não root. Portas abaixo de 1024 exigem privilégios de administrador. Verifique a disponibilidade com lsof -i :PORT (macOS/Linux) antes de usar.

Como posso lidar com erros 404 em um servidor HTTP Node.js?

Adicione um caso padrão na sua lógica de roteamento:

const requestListener = (req, res) => {
    if (req.url === '/') {
        res.writeHead(200);
        res.end('Home');
    } else {
        res.writeHead(404);
        res.end('Not Found');
    }
};

Sempre defina o código de status como 404 para rotas inexistentes, a fim de seguir os padrões HTTP.

Como posso servir arquivos estáticos sem o Express?

Use o módulo fs para ler arquivos e o módulo path para resolução segura de caminhos:

const fs = require('fs').promises;
const path = require('path');

const requestListener = async (req, res) => {
    const filePath = path.join(__dirname, 'public', req.url);
    try {
        const data = await fs.readFile(filePath);
        res.writeHead(200);
        res.end(data);
    } catch (error) {
        res.writeHead(404);
        res.end('File not found');
    }
};

Sempre valide os caminhos para prevenir ataques de travessia de diretório (veja o Passo 6 para uma implementação segura).

Posso usar módulos ES (import) com o módulo HTTP no Node.js?

Sim, o Node.js suporta módulos ES. Use qualquer um dos seguintes:

  1. Arquivo com extensão .mjs:
import http from 'http';
// ... rest of code
  1. Defina "type": "module" no package.json e, em seguida, use arquivos .js:
{
  "type": "module"
}

Observe que os Módulos ES não possuem __dirname—obtenha-o a partir de import.meta.url como mostrado no Passo 8.

Como faço para analisar os corpos de requisições POST com o módulo HTTP?

Os corpos de POST chegam como fluxos. Colete os pedaços e depois faça o parse:

const parseBody = (req) => {
    return new Promise((resolve, reject) => {
        let body = '';
        req.on('data', chunk => body += chunk.toString());
        req.on('end', () => {
            try {
                resolve(JSON.parse(body)); // For JSON
            } catch (error) {
                reject(error);
            }
        });
        req.on('error', reject);
    });
};

Este padrão funciona para JSON, dados de formulário ou qualquer tipo de conteúdo baseado em texto.

O módulo HTTP é adequado para uso em produção?

Sim, o módulo http está pronto para produção e é usado por muitas aplicações Node.js. No entanto, considere:

  • Gerenciamento de processos: Use PM2 ou similar para reinicializações automáticas
  • Proxy reverso: Execute atrás do Nginx/Apache para SSL e balanceamento de carga
  • Tratamento de erros: Implemente um tratamento de erros abrangente e registro de logs
  • Segurança: Valide entradas, sanitize caminhos, defina limites de tamanho para requisições
  • Desempenho: Armazene em cache arquivos estáticos, use cluster para operações que exigem CPU

Muitas aplicações de produção usam o módulo http diretamente, especialmente microsserviços e APIs leves. Para orientações completas de implantação em produção, veja Como Configurar uma Aplicação Node.js para Produção no Ubuntu 22.04.

Conclusão

Neste tutorial, você construiu servidores HTTP Node.js prontos para produção do zero. Você começou com uma resposta de texto básica, depois avançou para servir múltiplos tipos de conteúdo (JSON, CSV e HTML). Você implementou roteamento para lidar com diferentes URLs, analisou corpos de requisições POST, serviu arquivos estáticos de forma segura e adicionou tratamento de erros e registro de logs completos. Você também explorou tanto a sintaxe CommonJS quanto ES Module, além de considerações para implantação em produção.

Agora você está capacitado para criar servidores web que lidam com diferentes requisitos sem dependências de frameworks. Com esse conhecimento, você pode:

  • Crie APIs REST leves para microsserviços
  • Atenda sites de várias páginas com roteamento adequado
  • Gerenciar uploads e downloads de arquivos
  • Implementar lógica de middleware personalizada
  • Tome decisões informadas sobre quando os frameworks agregam valor versus quando adicionam complexidade desnecessária

Próximos passos:

  • Explore mais profundamente os fundamentos do Node.js: Retorne à série Como Programar em Node.js para uma cobertura completa
  • Explore a documentação oficial: A documentação do módulo HTTP do Node.js contém referências detalhadas da API e casos de uso avançados
  • Quando você precisar de recursos de framework: Considere os conceitos básicos do Express.js para desenvolvimento rápido com suporte a middleware e templating

Compreender esses fundamentos do HTTP torna você um desenvolvedor Node.js mais capaz. Quer você escolha usar frameworks ou trabalhar diretamente com o módulo http, agora você entende a mecânica subjacente que impulsiona todas as aplicações web em Node.js. Esse conhecimento ajuda a depurar problemas, otimizar o desempenho e arquitetar sistemas que escalem de forma eficaz.

Obrigado por aprender com a comunidade Linux-Console.net.