Introdução ao Node.js: JavaScript fora do navegador

Introdução ao Node.js: JavaScript fora do navegador

Em 2009 o JavaScript saiu da aba do navegador. O Node roda o mesmo motor V8, mas troca o DOM por acesso ao disco, à rede e ao sistema operacional — e é isso que este artigo apresenta: process, os módulos fs, path e os, CommonJS contra ES Modules, e um script de relatório que roda no terminal.
Javascript

• • 21 min de leitura

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 leituras recomendadas

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.

Comentários

Mais em Javascript

Tratamento de erros em requisições HTTP
Tratamento de erros em requisições HTTP

Código que só funciona quando tudo dá certo não está pronto para produção…

Promises: resolvendo o Callback Hell
Promises: resolvendo o Callback Hell

Uma Promise é o recibo de uma operação que ainda não terminou: você guarda o…

Padrões de Projeto em JavaScript
Padrões de Projeto em JavaScript

Dizer "isto é um Adapter" numa revisão economiza um parágrafo de explicação, e…