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

[109] 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 Referências

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

Filas de Tarefas e Processamento Assíncrono
Filas de Tarefas e Processamento Assíncrono

A fila separa aceitar o pedido de executá-lo: a API confirma em milissegundos…

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…

Arrays: criando e manipulando listas
Arrays: criando e manipulando listas

O sort() sem argumento ordena como se tudo fosse texto, e aí 10 vem antes de…