Até agora todo o JavaScript que escrevemos rodou no navegador. O navegador é um ambiente rico — tem o DOM, eventos, LocalStorage, APIs de geolocalização. Mas ele tem uma limitação fundamental: só existe dentro de uma aba.
O Node.js quebrou essa barreira em 2009. Pela primeira vez, JavaScript podia rodar no servidor — lendo arquivos do disco, abrindo conexões de rede, criando APIs, acessando bancos de dados. A mesma linguagem que você usa no front-end, agora também no back-end.
Este artigo apresenta o Node.js do zero — o que é, como funciona, o que muda em relação ao navegador e como dar os primeiros passos.
O que é Node.js?
Node.js é um ambiente de execução JavaScript construído sobre o motor V8 do Chrome. Não é uma linguagem nova — é JavaScript. O que muda é o ambiente onde ele roda e as APIs disponíveis.
┌──────────────────────────────────────────────────────┐
│ COMPARATIVO │
│ │
│ NAVEGADOR NODE.JS │
│ ───────────────────────── ────────────────── │
│ Motor: V8 (Chrome) Motor: V8 │
│ DOM, window, document Sem DOM │
│ fetch, localStorage Módulos nativos │
│ alert, confirm fs, path, os, http │
│ Limitado ao sandbox Acesso total ao OS │
│ Roda na aba do usuário Roda no servidor │
│ Distribuído ao cliente Fica no servidor │
└──────────────────────────────────────────────────────┘
Instalando e verificando o Node.js
# Verificar se já está instalado
node --version # ex: v20.11.0
npm --version # ex: 10.2.4
# Instalar (se necessário): https://nodejs.org
# Recomendado: versão LTS (Long Term Support)
Seu primeiro programa Node.js
Crie um arquivo ola.js:
// ola.js
console.log("Olá, Node.js!");
console.log("Versão:", process.version);
console.log("Sistema:", process.platform);
console.log("Diretório atual:", process.cwd());
Execute no terminal:
node ola.js
# Olá, Node.js!
# Versão: v20.11.0
# Sistema: linux
# Diretório atual: /home/usuario/projetos
Sem navegador. Sem HTML. Só JavaScript rodando direto no sistema operacional.
O objeto process — a ponte com o sistema
No navegador temos window. No Node, temos process:
// Informações do ambiente
console.log(process.version); // versão do Node
console.log(process.platform); // "linux", "win32", "darwin"
console.log(process.arch); // "x64", "arm64"
console.log(process.cwd()); // diretório de trabalho atual
console.log(process.env.HOME); // variáveis de ambiente
console.log(process.pid); // ID do processo
// Argumentos passados na linha de comando
// node script.js arg1 arg2
console.log(process.argv);
// ["node", "/path/script.js", "arg1", "arg2"]
const args = process.argv.slice(2); // remove "node" e o nome do arquivo
console.log(args); // ["arg1", "arg2"]
// Encerrar o processo
process.exit(0); // 0 = sucesso
process.exit(1); // 1 = erro
Sistema de módulos — CommonJS vs ES Modules
Node.js suporta dois sistemas de módulos. Entender a diferença é fundamental:
// ── CommonJS (padrão histórico do Node) ──────────
// Arquivos .js usam require() e module.exports
// matematica.js
function somar(a, b) { return a + b; }
function multiplicar(a, b) { return a * b; }
module.exports = { somar, multiplicar };
// ou exportar um por um:
module.exports.somar = somar;
// app.js
const { somar, multiplicar } = require("./matematica");
console.log(somar(3, 4)); // 7
console.log(multiplicar(3, 4)); // 12
// Módulos nativos do Node
const fs = require("fs");
const path = require("path");
const os = require("os");
// ── ES Modules (moderno, recomendado) ────────────
// Arquivos .mjs OU "type": "module" no package.json
// matematica.mjs
export function somar(a, b) { return a + b; }
export function multiplicar(a, b) { return a * b; }
export default { somar, multiplicar };
// app.mjs
import { somar, multiplicar } from "./matematica.mjs";
import fs from "fs";
Para novos projetos, prefira ES Modules. Para código legado e muitas bibliotecas, você ainda encontrará CommonJS.
Módulo fs — trabalhando com arquivos
O módulo fs (file system) é um dos mais usados no Node:
const fs = require("fs");
const path = require("path");
// ── Leitura ─────────────────────────────────────
// Síncrono — bloqueia a thread (evitar em servidores)
const conteudo = fs.readFileSync("arquivo.txt", "utf-8");
console.log(conteudo);
// Assíncrono com callback
fs.readFile("arquivo.txt", "utf-8", (erro, conteudo) => {
if (erro) {
console.error("Erro ao ler:", erro.message);
return;
}
console.log(conteudo);
});
// Assíncrono com Promise (recomendado)
const fsPromises = require("fs").promises;
// ou: const { readFile } = require("fs/promises");
async function lerArquivo(caminho) {
try {
const conteudo = await fsPromises.readFile(caminho, "utf-8");
return conteudo;
} catch (erro) {
if (erro.code === "ENOENT") {
throw new Error(`Arquivo não encontrado: ${caminho}`);
}
throw erro;
}
}
// ── Escrita ─────────────────────────────────────
// Sobrescreve o arquivo (cria se não existir)
await fsPromises.writeFile("saida.txt", "Olá, arquivo!", "utf-8");
// Adiciona ao final sem apagar o conteúdo existente
await fsPromises.appendFile("log.txt", `${new Date().toISOString()} — evento
`);
// ── Operações de diretório ───────────────────────
// Criar diretório (recursive: cria diretórios pais também)
await fsPromises.mkdir("pasta/subpasta", { recursive: true });
// Listar arquivos
const arquivos = await fsPromises.readdir("./");
console.log(arquivos);
// Verificar se existe
try {
await fsPromises.access("arquivo.txt");
console.log("Existe!");
} catch {
console.log("Não existe.");
}
// Informações do arquivo
const stats = await fsPromises.stat("arquivo.txt");
console.log(`Tamanho: ${stats.size} bytes`);
console.log(`Modificado: ${stats.mtime}`);
console.log(`É diretório: ${stats.isDirectory()}`);
// Renomear / mover
await fsPromises.rename("antigo.txt", "novo.txt");
// Deletar
await fsPromises.unlink("arquivo.txt");
await fsPromises.rm("pasta", { recursive: true }); // pasta com conteúdo
Módulo path — trabalhando com caminhos
Caminhos de arquivo são diferentes em Windows (\) e Unix (/). O módulo path resolve isso:
const path = require("path");
// Construir caminhos de forma segura (multiplataforma)
const caminho = path.join("/usuarios", "ana", "documentos", "arquivo.txt");
// "/usuarios/ana/documentos/arquivo.txt" no Unix
// "\usuarios\ana\documentos\arquivo.txt" no Windows
// Caminho absoluto a partir de caminho relativo
const absoluto = path.resolve("pasta", "arquivo.txt");
// "/projeto/atual/pasta/arquivo.txt"
// Partes do caminho
console.log(path.dirname("/pasta/subpasta/arquivo.txt")); // "/pasta/subpasta"
console.log(path.basename("/pasta/arquivo.txt")); // "arquivo.txt"
console.log(path.basename("/pasta/arquivo.txt", ".txt")); // "arquivo"
console.log(path.extname("/pasta/arquivo.txt")); // ".txt"
// __dirname e __filename (CommonJS) — diretório e arquivo atual
console.log(__dirname); // "/home/usuario/projeto"
console.log(__filename); // "/home/usuario/projeto/app.js"
// Equivalente em ES Modules
import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Módulo os — informações do sistema operacional
const os = require("os");
console.log(os.platform()); // "linux", "darwin", "win32"
console.log(os.arch()); // "x64", "arm64"
console.log(os.hostname()); // nome do computador
console.log(os.homedir()); // "/home/usuario"
console.log(os.tmpdir()); // "/tmp"
console.log(os.cpus().length); // número de CPUs
// Memória
const totalMem = os.totalmem();
const livMem = os.freemem();
console.log(`Memória total: ${(totalMem / 1024 / 1024 / 1024).toFixed(1)} GB`);
console.log(`Memória livre: ${(livMem / 1024 / 1024 / 1024).toFixed(1)} GB`);
console.log(`Uso: ${Math.round((1 - livMem / totalMem) * 100)}%`);
// Uptime
console.log(`Sistema rodando há ${Math.floor(os.uptime() / 3600)} horas`);
Um script prático — processador de arquivos
Vamos construir um script real que lê um arquivo CSV, processa os dados e gera um relatório:
// relatorio.js
const fs = require("fs/promises");
const path = require("path");
// Dados de exemplo — normalmente lidos de um arquivo real
const csvExemplo = `nome,departamento,salario,ativo
Ana Paula,Engenharia,8500,true
Carlos Silva,Design,6200,true
Beatriz Costa,Engenharia,9100,false
Diego Mendes,Marketing,5800,true
Elena Souza,Engenharia,7700,true
Fernando Lima,Design,6800,false
Gabriela Ramos,Marketing,5500,true
Hugo Santos,Engenharia,10200,true`;
function parsearCSV(texto) {
const linhas = texto.trim().split("\n");
const cabecalho = linhas[0].split(",");
return linhas.slice(1).map(linha => {
const valores = linha.split(",");
return cabecalho.reduce((obj, col, i) => {
const val = valores[i]?.trim();
// Conversão de tipos
if (val === "true") obj[col] = true;
else if (val === "false") obj[col] = false;
else if (!isNaN(val) && val !== "") obj[col] = Number(val);
else obj[col] = val;
return obj;
}, {});
});
}
function gerarRelatorio(funcionarios) {
const ativos = funcionarios.filter(f => f.ativo);
const inativos = funcionarios.filter(f => !f.ativo);
// Agrupamento por departamento
const porDepartamento = funcionarios.reduce((acc, f) => {
if (!acc[f.departamento]) {
acc[f.departamento] = { funcionarios: [], totalSalario: 0 };
}
acc[f.departamento].funcionarios.push(f);
acc[f.departamento].totalSalario += f.salario;
return acc;
}, {});
const totalSalarios = ativos.reduce((acc, f) => acc + f.salario, 0);
const mediaSalarial = totalSalarios / ativos.length;
const maiorSalario = Math.max(...funcionarios.map(f => f.salario));
const menorSalario = Math.min(...funcionarios.map(f => f.salario));
const separador = "═".repeat(50);
let relatorio = `
${separador}
RELATÓRIO DE RECURSOS HUMANOS
Gerado em: ${new Date().toLocaleString("pt-BR")}
${separador}
📊 RESUMO GERAL
${"─".repeat(50)}
Total de funcionários : ${funcionarios.length}
Funcionários ativos : ${ativos.length}
Funcionários inativos : ${inativos.length}
Folha de pagamento : R$ ${totalSalarios.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}
Salário médio : R$ ${mediaSalarial.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}
Maior salário : R$ ${maiorSalario.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}
Menor salário : R$ ${menorSalario.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}
🏢 POR DEPARTAMENTO
${"─".repeat(50)}`;
Object.entries(porDepartamento)
.sort((a, b) => b[1].totalSalario - a[1].totalSalario)
.forEach(([depto, dados]) => {
const media = dados.totalSalario / dados.funcionarios.length;
relatorio += `
${depto}
Funcionários : ${dados.funcionarios.length}
Total folha : R$ ${dados.totalSalario.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}
Média salarial: R$ ${media.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}
Membros: ${dados.funcionarios.map(f => f.nome.split(" ")[0]).join(", ")}
`;
});
relatorio += `
👥 LISTA DE FUNCIONÁRIOS
${"─".repeat(50)}
${"Nome".padEnd(20)} ${"Depto".padEnd(12)} ${"Salário".padEnd(12)} Status`;
relatorio += "\n" + "─".repeat(50);
funcionarios
.sort((a, b) => b.salario - a.salario)
.forEach(f => {
const status = f.ativo ? "✅ Ativo" : "❌ Inativo";
const salario = `R$ ${f.salario.toLocaleString("pt-BR")}`;
relatorio += `
${f.nome.padEnd(20)} ${f.departamento.padEnd(12)} ${salario.padEnd(12)} ${status}`;
});
relatorio += `
${separador}
`;
return relatorio;
}
async function main() {
// Argumentos da linha de comando
const args = process.argv.slice(2);
const arquivoEntrada = args[0] || null;
const arquivoSaida = args[1] || "relatorio.txt";
let csvTexto;
if (arquivoEntrada) {
console.log(`📂 Lendo arquivo: ${arquivoEntrada}`);
csvTexto = await fs.readFile(arquivoEntrada, "utf-8");
} else {
console.log("📝 Usando dados de exemplo...");
csvTexto = csvExemplo;
}
const funcionarios = parsearCSV(csvTexto);
console.log(`✅ ${funcionarios.length} funcionários carregados.`);
const relatorio = gerarRelatorio(funcionarios);
// Exibe no console
console.log(relatorio);
// Salva em arquivo
const caminhoSaida = path.resolve(arquivoSaida);
await fs.writeFile(caminhoSaida, relatorio, "utf-8");
console.log(`💾 Relatório salvo em: ${caminhoSaida}`);
}
main().catch(erro => {
console.error("❌ Erro fatal:", erro.message);
process.exit(1);
});
Execute:
node relatorio.js
# ou com arquivo real:
node relatorio.js funcionarios.csv relatorio.txt
REPL — o terminal interativo do Node
# Inicia o REPL (Read-Eval-Print Loop)
node
# Agora você pode digitar JavaScript diretamente:
> 2 + 2
4
> const nome = "Ana"
undefined
> `Olá, ${nome}!`
'Olá, Ana!'
> [1,2,3].map(n => n * 2)
[ 2, 4, 6 ]
> .exit # ou Ctrl+D para sair
Ótimo para testar código rapidamente sem criar arquivos.
O que Node.js tem que o navegador não tem
// Acesso ao sistema de arquivos
const fs = require("fs/promises");
await fs.readFile("/etc/hostname");
// Variáveis de ambiente (segredos, configurações)
const dbUrl = process.env.DATABASE_URL;
const porta = process.env.PORT || 3000;
// Informações do processo
process.pid; // ID do processo
process.memoryUsage(); // uso de memória
process.uptime(); // tempo rodando
// Criação de processos filhos
const { exec } = require("child_process");
exec("ls -la", (erro, stdout) => console.log(stdout));
// Servidor TCP/HTTP nativo
const http = require("http");
const servidor = http.createServer((req, res) => {
res.end("Olá!");
});
servidor.listen(3000);
// Streams — processamento de dados em fluxo
const readable = fs.createReadStream("arquivo-grande.txt");
readable.on("data", chunk => processar(chunk));
O que o navegador tem que Node não tem
// ❌ Não existe no Node.js:
document.querySelector("#btn");
window.localStorage;
window.alert("mensagem");
fetch("url"); // existe, mas só no Node 18+
navigator.geolocation;
Boas práticas ao começar com Node.js
# ✅ 1. Sempre crie um package.json no início do projeto
npm init -y
# ✅ 2. Use .env para variáveis sensíveis
# Crie um arquivo .env
# PORT=3000
# DATABASE_URL=postgres://...
# ✅ 3. Nunca commite .env no Git
# Adicione ao .gitignore:
echo ".env" >> .gitignore
echo "node_modules" >> .gitignore
# ✅ 4. Use versão LTS do Node
node --version # deve começar com número par: v20, v22...
// ✅ 5. Trate erros de processo
process.on("uncaughtException", (erro) => {
console.error("Erro não capturado:", erro);
process.exit(1);
});
process.on("unhandledRejection", (erro) => {
console.error("Promise rejeitada não tratada:", erro);
process.exit(1);
});
// ✅ 6. Prefira fs/promises em vez de fs com callbacks
// ❌ Evitar
fs.readFile("arquivo.txt", "utf-8", callback);
// ✅ Prefira
const conteudo = await fsPromises.readFile("arquivo.txt", "utf-8");
// ✅ 7. Use path.join para construir caminhos
// ❌ Evitar
const caminho = __dirname + "/pasta/" + arquivo;
// ✅ Prefira
const caminho = path.join(__dirname, "pasta", arquivo);
Tarefa para você
Construa um script de backup em Node.js que:
1. Lê todos os arquivos .txt e .json de um diretório
2. Cria um diretório "backup/YYYY-MM-DD" com a data atual
3. Copia todos os arquivos para o backup
4. Gera um arquivo "manifesto.json" no backup com:
- nome de cada arquivo
- tamanho em bytes
- data de modificação
- hash MD5 do conteúdo (use o módulo crypto nativo)
5. Exibe um resumo no terminal:
- Quantos arquivos copiados
- Tamanho total do backup
- Caminho do diretório de backup
// Dica — módulo crypto para MD5:
const crypto = require("crypto");
const hash = crypto.createHash("md5").update(conteudo).digest("hex");
Ver solução — o script de backup completo, com manifesto e MD5
// ---- backup.js
const fs = require("fs/promises");
const path = require("node:path");
const crypto = require("node:crypto");
const EXTENSOES = [".txt", ".json"];
// ---------------------------------------------------------------
// 1 — listar os arquivos que interessam
// ---------------------------------------------------------------
async function listarArquivos(diretorio) {
// withFileTypes evita um stat() por item só para saber se é pasta.
const entradas = await fs.readdir(diretorio, { withFileTypes: true });
return entradas
.filter((entrada) => entrada.isFile())
.filter((entrada) => EXTENSOES.includes(path.extname(entrada.name).toLowerCase()))
.map((entrada) => path.join(diretorio, entrada.name));
}
// ---------------------------------------------------------------
// 2 — o diretório do dia
// ---------------------------------------------------------------
function pastaDoDia(destino) {
const hoje = new Date();
// Montado à mão em vez de toISOString(): o ISO é UTC, e depois das
// 21h no Brasil ele já virou o dia seguinte — o backup da noite
// cairia na pasta de amanhã.
const data = [
hoje.getFullYear(),
String(hoje.getMonth() + 1).padStart(2, "0"),
String(hoje.getDate()).padStart(2, "0"),
].join("-");
return path.join(destino, "backup", data);
}
// ---------------------------------------------------------------
// 3, 4 e 5 — copiar, catalogar, resumir
// ---------------------------------------------------------------
async function fazerBackup(origem, destino = origem) {
const arquivos = await listarArquivos(origem);
if (arquivos.length === 0) {
console.log(`Nada para copiar em ${origem}`);
return null;
}
const pasta = pastaDoDia(destino);
// recursive: true cria a árvore inteira e NÃO reclama se já existir.
// É o substituto moderno do "if (!existsSync) mkdirSync" — que, além
// de mais longo, tem condição de corrida entre o teste e a criação.
await fs.mkdir(pasta, { recursive: true });
const manifesto = [];
let bytes = 0;
for (const caminho of arquivos) {
const nome = path.basename(caminho);
const [conteudo, info] = await Promise.all([
fs.readFile(caminho),
fs.stat(caminho),
]);
// O hash é do BUFFER, não de uma string decodificada: assim vale
// para qualquer arquivo e não depende do encoding usado na leitura.
const hash = crypto.createHash("md5").update(conteudo).digest("hex");
await fs.writeFile(path.join(pasta, nome), conteudo);
manifesto.push({
arquivo: nome,
tamanhoBytes: info.size,
modificadoEm: info.mtime.toISOString(),
md5: hash,
});
bytes += info.size;
}
const resumo = {
geradoEm: new Date().toISOString(),
origem: path.resolve(origem),
totalArquivos: manifesto.length,
totalBytes: bytes,
arquivos: manifesto,
};
await fs.writeFile(
path.join(pasta, "manifesto.json"),
JSON.stringify(resumo, null, 2),
"utf-8"
);
const emKB = (bytes / 1024).toFixed(1);
console.log(`✅ ${manifesto.length} arquivo(s) · ${emKB} KB`);
console.log(`📁 ${pasta}`);
return resumo;
}
// ---------------------------------------------------------------
// Verificar um backup — é para isto que o hash serve
// ---------------------------------------------------------------
// Manifesto que ninguém confere é enfeite. Esta função relê os
// arquivos copiados e compara com o hash gravado.
async function verificar(pasta) {
const resumo = JSON.parse(
await fs.readFile(path.join(pasta, "manifesto.json"), "utf-8")
);
const problemas = [];
for (const item of resumo.arquivos) {
try {
const conteudo = await fs.readFile(path.join(pasta, item.arquivo));
const hash = crypto.createHash("md5").update(conteudo).digest("hex");
if (hash !== item.md5) problemas.push(`${item.arquivo}: hash diferente`);
} catch {
problemas.push(`${item.arquivo}: ausente`);
}
}
console.log(
problemas.length === 0
? `✅ Backup íntegro (${resumo.arquivos.length} arquivos)`
: `❌ ${problemas.length} problema(s):\n ${problemas.join("\n ")}`
);
return problemas;
}
async function principal() {
const origem = process.argv[2] ?? ".";
const resumo = await fazerBackup(origem);
if (resumo) await verificar(pastaDoDia(origem));
}
// O catch existe para a promessa rejeitada não virar um
// UnhandledPromiseRejection, que desde o Node 15 derruba o processo
// com stack trace ilegível em vez de mensagem de erro.
principal().catch((erro) => {
console.error(`❌ ${erro.message}`);
process.exitCode = 1;
});
// ---------------------------------------------------------------
// O detalhe que morde: MD5 não serve para segurança
// ---------------------------------------------------------------
// Aqui ele está no lugar certo: detectar se o arquivo mudou, achar
// duplicata, conferir cópia. É rápido e o risco é irrelevante.
//
// O que NÃO se faz com MD5: guardar senha, assinar download,
// verificar integridade contra adulteração intencional. Colisão de
// MD5 é gerada em segundos desde 2005 — para esses casos, SHA-256:
//
// crypto.createHash("sha256").update(conteudo).digest("hex")
//
// E, para senha, nem hash de propósito geral serve: é bcrypt, scrypt
// ou argon2, que são lentos DE PROPÓSITO.
//
// Em arquivo grande, leia por stream em vez de carregar inteiro na
// memória — readFile de 2 GB estoura o heap:
//
// const { createReadStream } = require("node:fs");
// const { pipeline } = require("node:stream/promises");
// const hash = crypto.createHash("md5");
// await pipeline(createReadStream(caminho), hash);
// hash.digest("hex");
fs.mkdir(caminho, { recursive: true }) substitui o par existsSync + mkdirSync e ainda elimina a condição de corrida entre testar e criar. E cuidado com a data: toISOString() é UTC — depois das 21h no horário de Brasília, o backup da noite iria para a pasta do dia seguinte.
É o mesmo motor, o V8, rodando em outro lugar — e essa frase resume tanto o que muda quanto o que permanece. A linguagem é idêntica; o que troca é a vizinhança: no lugar de document e localStorage entram fs, path, os e process. Saber de que lado dessa fronteira mora cada coisa é o que evita procurar require no navegador e window no servidor.
Fontes e Referências
- Node.js Official Docs: https://nodejs.org/en/docs
- Node.js — fs module: https://nodejs.org/api/fs.html
- Node.js — path module: https://nodejs.org/api/path.html
- Node.js — os module: https://nodejs.org/api/os.html
- Node.js — process object: https://nodejs.org/api/process.html
- Node.js — sobre o runtime: https://nodejs.org/en/about
- Node.js Design Patterns — Mario Casciaro (Packt Publishing)
- Learning Node.js Development — Andrew Mead (Packt Publishing)
- Node.js — introdução oficial: https://nodejs.org/learn/getting-started/introduction-to-nodejs
Exercícios
Exercício 1
O arquivo é app.js, sem "type": "module" no package.json. O que acontece ao executar?
const fs = require("fs/promises");
const conteudo = await fs.readFile("dados.txt", "utf-8");
console.log(conteudo);
Ver resposta
✓ Resposta: Falha antes de rodar, com SyntaxError: await is only valid in async functions and the top level bodies of modules. O top-level await existe, mas só em módulos ES — e um arquivo .js sem "type": "module" é tratado como CommonJS, onde o await solto é proibido. A mensagem é boa e diz exatamente isso, mas o motivo confunde porque o mesmo código funciona quando colado no REPL do Node ou num arquivo .mjs. Há três saídas: renomear para .mjs, declarar "type": "module" no package.json e trocar os require por import, ou envolver tudo numa função async e chamá-la — o padrão main().catch(...) que o próprio script de relatório do artigo usa. Vale notar que a escolha não é só de sintaxe: em ES Modules não existem __dirname nem __filename, e é preciso reconstruí-los a partir de import.meta.url.
Exercício 2
Este código monta o caminho por concatenação. O que ele produz no Windows e o que quebra?
const caminho = __dirname + "/uploads/" + nomeArquivo;
// versus
const path = require("path");
const caminho2 = path.join(__dirname, "uploads", nomeArquivo);
Ver resposta
✓ Resposta: A concatenação produz algo como C:\projeto/uploads/foto.png — separadores misturados. Curiosamente, isso costuma funcionar, porque a API do Windows aceita a barra normal; a primeira versão não quebra por causa do separador. Ela quebra por outros dois motivos, bem mais sérios. O primeiro é a duplicação: se a variável já terminar em barra, o resultado ganha //, e caminhos com barras repetidas falham em algumas operações e estragam comparações de string. O segundo é de segurança: se nomeArquivo vier de um upload e contiver ../../etc/passwd, a concatenação obedece cegamente. O path.join normaliza o resultado — inclusive resolvendo os .. —, o que ajuda mas não basta: a proteção correta é comparar o caminho final resolvido com o diretório permitido, algo como path.resolve(destino).startsWith(path.resolve(base)), rejeitando o que escapar. Essa classe de falha tem nome, path traversal, e é das mais exploradas em servidores de arquivos.
Exercício 3
Um servidor HTTP atende muitas requisições por segundo e usa esta função. Qual é o problema?
const fs = require("fs");
function servirArquivo(req, res) {
const conteudo = fs.readFileSync("./template.html", "utf-8");
res.end(conteudo);
}
Ver resposta
✓ Resposta: O readFileSync bloqueia a thread única do Node enquanto o disco responde. E aqui a palavra "única" é o ponto: diferente de um servidor com uma thread por requisição, o Node atende todas no mesmo laço de eventos — enquanto essa leitura acontece, nenhuma outra requisição avança, nenhum timer dispara, nenhuma resposta é enviada. Uma leitura de 5 ms, com trezentas requisições por segundo, transforma o servidor num gargalo, e o sintoma é enganoso: a métrica de CPU fica baixa, porque o processo está esperando, não calculando. A versão correta é await fs.promises.readFile(...), que devolve o controle ao laço durante a espera. E, no caso específico de um template que nunca muda, a melhor correção é outra: ler uma vez na inicialização e guardar em memória — a operação mais rápida é a que não acontece. As funções *Sync têm lugar legítimo em scripts de linha de comando e na fase de boot, onde não há concorrência a atrapalhar.
Exercício 4
O que estas duas linhas imprimem, e por que a segunda existe?
// node script.js relatorio.csv --verbose
console.log(process.argv); // A
console.log(process.argv.slice(2)); // B
Ver resposta
✓ Resposta: A imprime quatro elementos: o caminho do executável do Node, o caminho absoluto do script, e só então "relatorio.csv" e "--verbose". B imprime apenas os dois últimos. O slice(2) existe justamente para descartar os dois primeiros, que são fixos e quase nunca interessam — é um idioma tão comum no Node que aparece em praticamente todo script de linha de comando. Dois detalhes que evitam surpresa: os valores são sempre strings, então um argumento numérico precisa de Number() antes de qualquer conta, exatamente como acontece com FormData e com CSV; e o process.argv não interpreta nada — --verbose chega como texto literal, sem virar uma flag booleana, e agrupar -abc ou aceitar --porta=3000 é trabalho seu. Para qualquer coisa além de dois ou três argumentos posicionais, vale usar o parseArgs nativo do módulo node:util, disponível desde o Node 18.
Exercício 5
Por que document não existe no Node e require não existe no navegador, se os dois rodam o mesmo motor V8?
// no Node:
document.querySelector("#btn"); // ReferenceError: document is not defined
// no navegador:
require("fs"); // ReferenceError: require is not defined
Ver resposta
✓ Resposta: Porque nenhum dos dois faz parte do JavaScript. O V8 implementa apenas a linguagem — sintaxe, tipos, Array, Promise, Math, JSON —, e tudo o mais é fornecido pelo ambiente que hospeda o motor. O navegador acrescenta o DOM, window, localStorage e alert, porque precisa desenhar páginas; o Node acrescenta fs, path, process e o sistema de módulos CommonJS, porque precisa falar com o sistema operacional. Um não tem o do outro simplesmente porque não faria sentido: não há tela no servidor, e dar acesso irrestrito ao disco a uma página de site seria catastrófico. Essa fronteira explica um monte de confusão do dia a dia — por que fetch só chegou ao Node na versão 18, por que setTimeout se comporta de forma ligeiramente diferente nos dois, e por que uma biblioteca "isomórfica" precisa detectar onde está rodando. A regra para se orientar: se está no MDN sob Web APIs, é do navegador; se está na documentação do Node, é do servidor; se está em JavaScript reference, existe nos dois.