Se você já programa em JavaScript no navegador e quer dar o próximo passo, levando essa linguagem para o lado do servidor, então prepare-se: este tutorial vai te guiar, passo a passo, na construção de aplicações web usando apenas os módulos nativos do Node.js. Sem frameworks, sem mágica. Apenas você, o JavaScript e o motor que executa código fora do navegador.
A proposta aqui é direta: ao final deste post, você terá criado seu primeiro servidor HTTP, entendido como ele funciona internamente, aprendido a tratar várias rotas, separado o HTML do código JavaScript, e ainda terá um desafio prático para consolidar tudo o que viu. Vamos lá?
Por que começar pelo módulo HTTP nativo?
Antes de mergulhar no código, é importante entender o contexto. O Node.js é multiprotocolo. Isso significa que com ele você consegue trabalhar com diversos protocolos de rede: HTTP, HTTPS, FTP, SSH, DNS, TCP, UDP e WebSockets, entre outros disponibilizados pela comunidade. No entanto, quando o assunto é desenvolvimento web, o protocolo HTTP é, de longe, o mais utilizado e o que conta com a maior quantidade de módulos prontos para uso.
Toda aplicação web precisa de um servidor para disponibilizar seus recursos. E aqui está uma característica interessante do Node.js: quando você desenvolve com ele, está, na prática, criando uma aplicação middleware. Ou seja, além de programar as funcionalidades da sua aplicação, você também é responsável por escrever códigos de configuração da infraestrutura do servidor.
À primeira vista, isso pode parecer trabalhoso. Afinal, o Node.js exige o mínimo de configurações para servir uma aplicação, o que deixa nas suas mãos a tarefa de definir os detalhes. Mas é justamente aí que mora a vantagem: você consegue customizar ao máximo seu servidor, ajustando detalhes que permitem desenvolver algo extremamente performático e sob controle total.
Existem módulos de mais alto nível como Connect, Express, Geddy, CompoundJS, Sails e outros, que já vêm com configurações mínimas prontas, permitindo trabalhar com arquiteturas RESTful, padrões MVC e conexões em tempo real com WebSockets. Eles são ótimos para quem precisa de produtividade. Porém, mesmo que você venha a usar esses frameworks no futuro, é fundamental entender o módulo nativo HTTP, porque todos esses frameworks se apoiam nele como base.
Por isso vamos começar do zero, do jeito mais cru.
Criando nossa primeira aplicação web
A clássica aplicação “Hello World” é o ponto de partida ideal. Crie um arquivo chamado hello_server.js e coloque o seguinte conteúdo dentro dele:
var http = require('http');
var server = http.createServer(function(request, response){
response.writeHead(200, {"Content-Type": "text/html"});
response.write("<h1>Hello World!</h1>");
response.end();
});
server.listen(3000);
Esse é um exemplo bem enxuto, mas que já mostra a estrutura básica de qualquer servidor Node.js. Vamos dissecá-lo linha por linha:
-
var http = require('http');— Carrega o módulo nativohttp. Tudo que diz respeito a servir conteúdo via HTTP está nesse pacote. -
http.createServer(...)— Cria uma instância de servidor. A função passada como argumento é o que chamamos de callback, e ela só será executada quando o servidor receber uma requisição. - O callback recebe dois parâmetros:
request(a requisição feita pelo cliente) eresponse(o objeto usado para enviar a resposta de volta). -
response.writeHead(200, {...})— Escreve o cabeçalho da resposta. O número200é o status HTTP de sucesso, e o objeto define que estamos enviando conteúdo HTML. -
response.write("<h1>Hello World!</h1>");— Adiciona o conteúdo HTML que será enviado ao cliente. -
response.end();— Encerra a resposta. Sem isso, o navegador ficaria esperando indefinidamente. -
server.listen(3000);— Coloca o servidor para escutar requisições na porta 3000.
Agora salve o arquivo, vá até o terminal, navegue até a pasta onde ele está e rode:
node hello_server.js
Em seguida, abra seu navegador e acesse http://localhost:3000. Você verá uma página com o cabeçalho “Hello World!” em destaque. Pronto, você acabou de subir seu primeiro servidor web em Node.js, sem precisar de Apache, Nginx ou qualquer outro intermediário. Bem-vindo ao desenvolvimento back-end com JavaScript.
Como funciona um servidor HTTP por baixo dos panos?
Para evoluir de simples “copia e cola” para um desenvolvedor que realmente entende o que está fazendo, é essencial compreender o mecanismo que faz o servidor responder às requisições. E esse mecanismo se chama Event Loop.
Um servidor Node.js utiliza o Event Loop como peça central. Ele é o responsável por lidar com a emissão de eventos. Na prática, a função http.createServer() apenas levanta o servidor. O callback que passamos como argumento (function(request, response)) só é executado quando o servidor recebe uma requisição. Para que isso aconteça, o Event Loop fica constantemente verificando se o servidor foi requisitado. Quando uma requisição chega, ele emite um evento que dispara a execução do nosso callback.
Esse modelo é fundamentalmente diferente do que acontece em linguagens com modelo de threads bloqueantes. Aqui, o servidor não cria uma nova thread a cada requisição: ele despacha o trabalho e segue ouvindo. É por isso que o Node.js é tão eficiente para aplicações com muita E/S (entrada e saída).
O Node.js trabalha intensamente com chamadas assíncronas que respondem por meio de callbacks. Vamos ver isso na prática. Se quisermos receber uma notificação no console assim que o servidor estiver de pé, basta passar uma função como segundo argumento para server.listen():
server.listen(3000, function(){
console.log('Servidor Hello World rodando!');
});
O método listen também é assíncrono. Isso significa que você só saberá que o servidor está de pé quando o Node invocar a sua função de callback. Não há retorno imediato dizendo “pronto, está rodando”. Em vez disso, o Node avisa quando estiver pronto.
Se você está começando agora com JavaScript, pode estranhar essa prática de passar funções como argumento para todo lado. Isso se chama higher-order functions (funções de ordem superior) e é absolutamente normal no mundo JavaScript. Mas, em algum momento, seu código pode começar a ficar difícil de ler por causa de muitos blocos aninhados. Quando isso acontecer, você pode separar essas funções e dar nomes mais significativos a elas. Veja a diferença:
var http = require('http');
var atendeRequisicao = function(request, response) {
response.writeHead(200, {"Content-Type": "text/html"});
response.write("<h1>Hello World!</h1>");
response.end();
}
var server = http.createServer(atendeRequisicao);
var servidorLigou = function() {
console.log('Servidor Hello World rodando!');
}
server.listen(3000, servidorLigou);
O comportamento é exatamente o mesmo, mas o código fica mais fácil de ler. Essa técnica é especialmente útil quando os callbacks começam a crescer e a se aninhar. Manter funções pequenas e bem nomeadas é uma prática que vai te poupar muita dor de cabeça no futuro.
Trabalhando com diversas rotas
Até agora, nosso servidor só responde à rota raiz (/). Independentemente do endereço acessado, ele devolve sempre o mesmo HTML. Não é assim que aplicações reais funcionam. Precisamos diferenciar requisições para /sobre, /contato, /produtos e tantas outras rotas que uma aplicação pode ter.
Vamos adicionar uma rota chamada /bemvindo, que exibirá uma página de boas-vindas, e uma rota genérica que servirá como página de erro para qualquer endereço não reconhecido. Crie o arquivo hello_server3.js:
var http = require('http');
var server = http.createServer(function(request, response){
response.writeHead(200, {"Content-Type": "text/html"});
if(request.url == "/"){
response.write("<h1>Página principal</h1>");
} else if(request.url == "/bemvindo"){
response.write("<h1>Bem-vindo :)</h1>");
} else {
response.write("<h1>Página não encontrada :(</h1>");
}
response.end();
});
server.listen(3000, function(){
console.log('Servidor rodando!');
});
Rode o servidor com node hello_server3.js e teste no navegador. Acesse http://localhost:3000/, depois http://localhost:3000/bemvindo e, por último, algum caminho inventado como http://localhost:3000/qualquer-coisa. Você verá três páginas diferentes.
Repare como o roteamento aqui foi feito de forma bem rústica: usamos uma cadeia de if e else, e a leitura da URL é feita por meio da propriedade request.url, que devolve uma string com o caminho digitado pelo usuário. Funciona, mas em um projeto de verdade, com dezenas ou centenas de rotas, essa abordagem se tornaria um pesadelo de manutenção.
Além disso, URLs reais costumam carregar informação além do caminho. Existem dois padrões muito comuns:
-
Query strings: parâmetros após o
?na URL, como em?nome=joao&idade=30. -
Path: o caminho em si, como
/admin/usuarios.
Para lidar com esses padrões de forma estruturada, o Node.js oferece um módulo nativo chamado url, responsável por fazer parser e formatação de URLs. Vamos ver como capturar valores de uma query string. Crie o arquivo url_server.js:
var http = require('http');
var url = require('url');
var server = http.createServer(function(request, response){
response.writeHead(200, {"Content-Type": "text/html"});
response.write("<h1>Dados da query string</h1>");
var result = url.parse(request.url, true);
for(var key in result.query){
response.write("<h2>"+ key +" : "+ result.query[key] +"</h2>");
}
response.end();
});
server.listen(3000, function(){
console.log('Servidor http.');
});
Salve, execute com node url_server.js e acesse algo como http://localhost:3000/?nome=maria&cidade=saopaulo. Você verá os pares chave-valor exibidos na página.
A função url.parse(request.url, true) faz o parser da URL recebida. O segundo argumento true indica que a query string deve ser convertida em um objeto JavaScript (caso contrário, ela viria como uma string só). O retorno dessa função traz vários atributos úteis, que vale a pena conhecer:
-
href: a URL completa. Exemplo:
http://user:pass@host.com:8080/p/a/t/h?query=string#hash -
protocol: o protocolo usado. Exemplo:
http -
host: o domínio com a porta. Exemplo:
host.com:8080 -
auth: dados de autenticação embutidos. Exemplo:
user:pass -
hostname: apenas o domínio. Exemplo:
host.com -
port: a porta. Exemplo:
8080 -
pathname: o caminho. Exemplo:
/p/a/t/h -
search: a query string em formato bruto. Exemplo:
?query=string -
path: a concatenação de
pathnamecomsearch. Exemplo:/p/a/t/h?query=string -
query: a query string já convertida em JSON. Exemplo:
{ query: 'string' } -
hash: âncora da URL. Exemplo:
#hash
Em resumo, o módulo url permite organizar toda e qualquer URL da aplicação de maneira estruturada, deixando seu código muito mais limpo e legível do que se você tivesse que separar tudo manualmente com split e expressões regulares.
Separando o HTML do JavaScript
Você já deve ter percebido um problema: até aqui, nosso HTML está escrito como string dentro do código JavaScript. Para páginas simples isso pode até funcionar, mas em qualquer aplicação real teremos HTML extenso, com CSS, imagens e estruturas complexas. Misturar tudo em strings JavaScript é receita para um código impossível de manter.
A boa prática é separar o HTML em arquivos próprios, com a extensão .html, e fazer a aplicação ler esses arquivos quando precisar respondê-los ao usuário. Para isso, vamos usar mais um módulo nativo: o File System, conhecido pela abreviação fs.
O módulo fs é responsável por manipular arquivos e diretórios do sistema operacional. O que ele tem de mais interessante é oferecer praticamente todas as suas funções em duas versões: uma assíncrona e outra síncrona. Por convenção, as funções com sufixo Sync são as síncronas. Veja o exemplo abaixo, que mostra as duas formas de ler um arquivo:
var fs = require('fs');
// Forma assíncrona
fs.readFile('/index.html', function(erro, arquivo){
if (erro) throw erro;
console.log(arquivo);
});
// Forma síncrona
var arquivo = fs.readFileSync('/index.html');
console.log(arquivo);
A função fs.readFile() faz uma leitura assíncrona. Depois que o arquivo termina de carregar, a função callback é invocada com dois argumentos: o erro (caso tenha ocorrido) e o conteúdo do arquivo. Já a fs.readFileSync() faz uma leitura síncrona: o programa para tudo e espera o arquivo ser completamente lido para depois prosseguir.
No mundo Node.js, a forma assíncrona é quase sempre a melhor escolha. O servidor não pode ficar bloqueado esperando uma leitura de disco terminar enquanto outras requisições chegam. Por isso, vamos usar a versão assíncrona daqui em diante.
Uma observação importante: o módulo File System não é 100% consistente entre os sistemas operacionais. Algumas funções são específicas para Linux, OS X e Unix, e outras só funcionam no Windows. Sempre que for usar funções menos comuns, vale a pena consultar a documentação oficial do Node.js para evitar surpresas.
Agora vamos juntar HTTP com File System para servir uma página HTML real. Crie o arquivo site_pessoal.js:
var http = require('http');
var fs = require('fs');
var server = http.createServer(function(request, response){
// A constante __dirname retorna o diretório raiz da aplicação.
fs.readFile(__dirname + '/index.html', function(err, html){
response.writeHeader(200, {'Content-Type': 'text/html'});
response.write(html);
response.end();
});
});
server.listen(3000, function(){
console.log('Executando Site Pessoal');
});
Repare em dois detalhes muito importantes:
- A constante
__dirnameé fornecida automaticamente pelo Node.js e devolve o caminho absoluto do diretório onde o arquivo atual está sendo executado. Sem isso, dependendo de onde o comando é rodado, o caminho relativo poderia quebrar. - A leitura é assíncrona: a renderização do HTML acontece dentro do callback, ou seja, só depois que o arquivo for completamente lido.
Para esse código funcionar, você precisa criar um arquivo index.html no mesmo diretório. Algo simples como:
<!DOCTYPE html>
<html>
<head>
<title>Olá este é o meu site pessoal!</title>
</head>
<body>
<h1>Bem vindo ao meu site pessoal</h1>
</body>
</html>
Rode node site_pessoal.js e acesse http://localhost:3000. A diferença em relação aos exemplos anteriores é que, agora, qualquer alteração no index.html pode ser feita sem mexer no código JavaScript. Só recarregar o navegador e pronto. Bem-vindo à separação de responsabilidades.
Desafio: implementar um roteador de URLs
Agora chegou a melhor parte: colocar a mão na massa e juntar tudo o que você aprendeu em um único projeto. Você já conhece os três módulos nativos essenciais para servir conteúdo web: o http para subir o servidor, o url para fazer parser das rotas e o fs para ler arquivos HTML do disco. Hora de combiná-los em algo útil.
O desafio é simples na descrição, mas exige atenção: você vai construir um pequeno roteador de URLs, que renderiza arquivos HTML diferentes dependendo do caminho que o usuário digitar no navegador.
Regras do desafio
- Crie 3 arquivos HTML:
artigos.html,contato.htmleerro.html. - Coloque qualquer conteúdo dentro de cada um dos arquivos. Pode ser um simples
<h1>com o nome da página. - Quando o usuário digitar o path
/artigosno navegador, o servidor deve renderizar oartigos.html. - Quando o usuário digitar
/contato, o servidor deve renderizar ocontato.html. - Para qualquer outro path diferente de
/artigose/contato, o servidor deve renderizar oerro.html. - Toda a leitura de arquivos HTML deve ser feita de forma assíncrona.
- A rota principal
/(raiz) deve renderizar oartigos.htmlcomo padrão.
Dicas importantes
1. Use o retorno da função url.parse() para capturar o pathname digitado e renderizar o HTML correspondente. Se o pathname estiver vazio ou for /, significa que deve renderizar a página de artigos. Se o valor for diferente do nome de qualquer arquivo HTML que você tem, renderize a página de erro.
2. Você pode inserir o conteúdo HTML diretamente dentro da função response.end(html), economizando uma linha de código. Em vez de fazer:
response.write(html);
response.end();
Você pode fazer apenas:
response.end(html);
O comportamento é o mesmo, mas o código fica mais conciso.
3. Você pode usar uma estrutura condicional simples para decidir qual arquivo carregar, baseando-se no pathname. Algo na linha de “se o pathname é /artigos ou /, carregue artigos.html; se é /contato, carregue contato.html; caso contrário, carregue erro.html”.
Estratégia recomendada
Antes de sair codando, pense no fluxo da aplicação:
- Subir um servidor HTTP na porta 3000.
- Em cada requisição, obter a URL chamada.
- Fazer parser dessa URL para extrair o
pathname. - Decidir qual arquivo HTML carregar com base nesse
pathname. - Ler o arquivo de forma assíncrona.
- Devolver o conteúdo do arquivo como resposta HTTP com status 200 e content-type
text/html.
Esse exercício, embora pareça pequeno, te coloca em contato com a arquitetura básica de qualquer aplicação web em Node.js. Frameworks como o Express, que você provavelmente vai aprender mais tarde, fazem exatamente isso por baixo dos panos: leem a URL, decidem o que fazer, e respondem.
Por que esse desafio importa?
Quando você implementa o roteador manualmente, sem depender de bibliotecas externas, você passa a entender o que acontece quando configura uma rota em qualquer framework. Você sabe que o framework está lendo request.url, fazendo um parser, escolhendo um handler e respondendo. Esse conhecimento é o que separa quem usa ferramentas “por mágica” de quem realmente domina a tecnologia.
Tente fazer o desafio sozinho antes de procurar uma solução pronta. Se travar, releia as seções anteriores deste tutorial. Todas as peças que você precisa já foram apresentadas. O exercício é justamente combiná-las.
Conclusão
Em poucas linhas de código, você passou de zero a um pequeno servidor web funcional. E mais do que isso, você entendeu por que cada peça do quebra-cabeça existe.
Recapitulando o caminho percorrido:
- Você descobriu que o Node.js é multiprotocolo e que, no contexto web, o HTTP é o protocolo mais usado e mais bem suportado.
- Aprendeu que cada aplicação Node.js é também uma aplicação middleware, exigindo que você programe não só o seu domínio, mas também aspectos da infraestrutura, com a vantagem da customização total.
- Construiu seu primeiro servidor com o módulo nativo
http, entendendo cada parâmetro:writeHead,write,endelisten. - Compreendeu o papel do Event Loop como motor que orquestra a execução dos callbacks de forma assíncrona, sem bloquear o servidor.
- Implementou um roteamento simples com
if/elseusandorequest.url, e depois evoluiu para o uso do módulo nativourlcomurl.parse, conhecendo todos os atributos disponíveis:href,protocol,host,auth,hostname,port,pathname,search,path,queryehash. - Aplicou boas práticas separando o HTML do JavaScript, lendo arquivos com o módulo
fs, e entendendo as diferenças entre leitura síncrona (readFileSync) e assíncrona (readFile), além de conhecer a utilíssima constante__dirname. - Recebeu um desafio que combina todos esses conceitos em uma única aplicação prática.
Tudo isso usando apenas módulos nativos, sem instalar uma única dependência via npm. Esse é o tipo de fundamento que vai te acompanhar pelo resto da sua jornada com Node.js, independentemente do framework que você venha a adotar.
A partir daqui, o céu é o limite. Você pode evoluir esse roteador para suportar parâmetros dinâmicos (algo como /artigos/:id), pode adicionar suporte a métodos HTTP diferentes (GET, POST, PUT, DELETE) e pode até começar a servir conteúdo JSON em vez de HTML, criando assim uma API REST completa, ainda sem frameworks.
Mas se em algum momento você sentir que está reinventando a roda, saiba que existe um ecossistema gigantesco de bibliotecas prontas para te ajudar. E quando chegar nesse ponto, você não vai ser apenas mais um copiador de exemplos de internet: vai saber exatamente o que aquelas bibliotecas estão fazendo, porque já fez na unha.
Boa codificação, e até o próximo tutorial.

Top comments (0)