Dia 1 — Node.js: rodar JavaScript fora do navegador
O que e Node.js e por que ele existe
O que é Node.js e por que ele existe
Node.js não é outra linguagem. É o mesmo JavaScript do 1º trimestre — o motor
V8, o mesmo Promise, o mesmo async/await — rodando dentro de um processo
do sistema operacional, em vez de dentro de uma aba do navegador. O arquivo .js
não muda de sintaxe. Muda o que existe em volta dele: onde o navegador oferece
document e window, o Node oferece process e os módulos de sistema.
| Onde | O que a linguagem faz sozinha | O que ela precisa de fora |
|---|---|---|
| Navegador | altera a árvore da página | a página precisa existir |
| Node | escreve no terminal e na rede | o processo precisa subir |
Essa é a virada do trimestre: no navegador a linguagem sozinha produz efeito
visível; aqui nada acontece sem subir um processo, sem rota e sem banco.
O termo que aparece em toda documentação é backend: o código que roda no
servidor, respondendo requisições em vez de alterar a tela de quem está diante
dela. O JavaScript no servidor é a mesma linguagem — mesmo motor, mesma
sintaxe, mesmas Promise e async/await. O que muda não é o que se escreve,
é o que existe em volta do arquivo.
Rodar um programa de Node.js é entregar um arquivo .js ao runtime pelo prompt
do sistema, o mesmo onde se roda git e ls. A linha de comando é o nome
desse prompt: o lugar de onde o processo é chamado e onde a saída aparece.
node -v mostra a versão instalada e node app.js entrega o arquivo. Não
existe duplo clique nem aba: o ambiente de execução aqui é o processo do
sistema operacional, e por isso todo exemplo deste material termina em
console.log — o efeito deixou de ser a página mudar e passou a ser uma linha
no terminal ou uma resposta de HTTP.
Node.js nasceu na Heroku, em 2009, e a manutenção do projeto foi assumida pela
Node.js Foundation, hoje a OpenJS Foundation, que publica as versões.
O que o runtime entrega além da linguagem
O Node empacota o motor V8 do Chrome e liga nele o que o navegador não expõe:
sistema de arquivos, sockets, processo, sinais, criptografia. Tudo isso chega
por require, e todo módulo nativo tem o prefixo node:.
const http = require('node:http'); // servidor e cliente HTTP const fs = require('node:fs'); // arquivos const path = require('node:path'); // caminhos do sistema const os = require('node:os'); // information do sistema
O prefixo node: é opcional no pacote de módulos nativo, mas é o que a
documentação usa e o que separa require('node:fs') de um pacote chamado fs
que alguém publicou no registro.
process.version e process.versions
process.version é a versão do Node; process.versions é um objeto com uma
chave por componente empacotado no runtime, incluindo v8. É o lugar de olhar
quando a dúvida é "o que este processo é, de verdade" — o mesmo espírito de
capturar a versão do banco em vez de escrevê-la à mão.
console.log(process.version); // v24.21.0 console.log(process.versions.v8); // versão do motor console.log(Object.keys(process.versions));
Ordem de execução: microtask antes de macrotask
O console.log é síncrono e sai na hora. setTimeout(fn, 0) vira uma
macrotask. Promise.resolve().then(fn) vira uma microtask — e a microtask
sempre sai antes de qualquer macrotask, mesmo pedida depois do timer.
O exemplo imprime 1, 2, 3, 4 nessa ordem, e o motivo é esse: o Node
entrega o controle, vai buscar o próximo evento e volta. É o mesmo event loop
que faz um servidor HTTP atender várias requisições sem travar: o processo
nunca "para" esperando I/O, ele volta para a fila.
Exemplo
'use strict'; // Exemplo da aula 1 do dia 1: o runtime lido de dentro do proprio processo. // // A tese da aula e que o arquivo que roda em Node e o mesmo JavaScript do // navegador. O que muda e quem esta em volta: em vez das globais de pagina // web, existe `process`, que e o proprio processo do sistema operacional. console.log('node:', process.version); console.log('executavel:', process.execPath); console.log('plataforma:', process.platform, '| arquitetura:', process.arch); // O V8 e o motor de JavaScript. O Node nao reescreve a linguagem: ele empacota // o mesmo motor do Chrome e liga nele o que o navegador nao tem — arquivo, // socket, processo. Por isso `Promise`, `async` e `Map` funcionam igual nos dois. console.log('motor V8:', process.versions.v8); console.log('componentes empacotados:', Object.keys(process.versions).join(', ')); // Estas quatro sao globais: existem sem `require`, exatamente como no navegador. console.log('sem require:', typeof process, typeof Buffer, typeof global, typeof setTimeout); // A ordem das filas. `console.log` e sincrono e sai na hora; `setTimeout` vira // macrotask; `Promise.resolve().then` vira microtask. A microtask sai ANTES de // qualquer macrotask, mesmo tendo sido pedida depois do timer. console.log(''); console.log('--- ordem de execucao ---'); console.log('1 - sincrono, sai na hora'); setTimeout(() => { console.log('4 - macrotask (setTimeout), resolvido por ultimo'); console.log('agora o event loop esvaziou e o processo pode terminar'); }, 0); Promise.resolve().then(() => console.log('3 - microtask (then), antes de qualquer macrotask')); console.log('2 - sincrono, ainda antes da microtask'); // A ordem e 1, 2, 3, 4. E o mesmo event loop que faz um servidor HTTP atender // varias requisicoes: o Node nunca "para" esperando I/O, ele entrega o controle // e volta para buscar o proximo evento. Por isso o script acima ja terminou de // ler quando estas ultimas duas linhas aparecem.
Saída real
node: v24.21.0 executavel: /root/.hermes/node/bin/node plataforma: linux | arquitetura: x64 motor V8: 13.6.233.17-node.53 componentes empacotados: node, acorn, ada, amaro, ares, brotli, cldr, icu, llhttp, merve, modules, napi, nbytes, ncrypto, nghttp2, nghttp3, ngtcp2, openssl, simdjson, simdutf, sqlite, tz, undici, unicode, uv, uvwasi, v8, zlib, zstd sem require: object function object function --- ordem de execucao --- 1 - sincrono, sai na hora 2 - sincrono, ainda antes da microtask 3 - microtask (then), antes de qualquer macrotask 4 - macrotask (setTimeout), resolvido por ultimo agora o event loop esvaziou e o processo pode terminar
O primeiro programa no terminal
O primeiro programa no terminal
Um programa de terminal responde a três perguntas: com qual nome ele foi
chamado, o que recebeu de argumento e como termina. As três têm resposta em
process, e as três mudam de comportamento entre o navegador e o Node.
node app.js 42 --semente
node arquivo.js e o node REPL
node arquivo.js é o comando inteiro: o runtime, o caminho do arquivo e o que
vier depois são argumentos, todos eles texto. É o mesmo arquivo que roda no
servidor de produção, sem mudança nenhuma.
node REPL é o mesmo runtime sem arquivo nenhum. Ele abre um prompt com >
esperando uma expressão, imprime o resultado e repete a espera; > 2 + 2
devolve 4 e > .exit fecha a sessão. REPL é a sigla de read-eval-print loop
— ler, avaliar, imprimir, repetir — e é o jeito mais rápido de conferir uma
hipótese sobre a linguagem sem abrir editor nenhum.
process.argv, process.cwd e __dirname
process.argv é uma lista: argv[0] é o executável do Node, argv[1] é o
caminho do arquivo e o resto são os argumentos de quem chamou — sempre texto.
process.cwd() é o diretório de trabalho, que não é necessariamente o
diretório do arquivo. __dirname é o diretório do próprio arquivo, e só existe
em CommonJS.
| Variável | O que é |
|---|---|
process.argv | [executavel, arquivo, ...argumentos] |
process.execPath | caminho do binário do Node em execução |
process.cwd() | diretório de trabalho do processo |
__dirname | diretório do arquivo (CommonJS) |
__filename | caminho completo do arquivo (CommonJS) |
A distinção entre process.argv[1] e __filename é o tipo: argv[1] vem da
linha de comando e pode nem ser um caminho resolvido, enquanto __filename é
sempre absoluto. Para montar caminho de arquivo, o que se usa é __dirname.
Código de saída: process.exitCode e process.exit
O shell lê o resultado do processo em $?, e esse número é o exit code do
programa. 0 é sucesso, qualquer outra coisa é falha — é o que um script de
deploy, um CI e um && no terminal usam para decidir se seguem.
O console.log no Node escreve a linha de saída do programa, e o exit code é o
outro canal: não aparece em lugar nenhum do texto impresso.
process.exitCode = 1 marca a saída sem matar o processo na hora — o Node
termina sozinho quando a fila esvazia, e o que ainda estava pendente termina.
process.exit(1) mata na hora e descarta o que não foi escrito ainda; ele é a
exceção, não a regra.
Os dois fluxos que o shell enxerga são stdout e stderr.
Um erro no terminal que ninguém tratou sai pelo segundo: stderr, com exit code
1 e o stack trace inteiro. O stdout fica limpo — é por isso que quem loga
servidor separa erro de resultado redirecionando só o segundo fluxo.
Stack trace: o que acontece sem tratamento
Erro não tratado derruba o processo: TypeError sai com código 1 e o stack
inteiro vai para o stderr. Erro tratado vira uma variável e o programa segue.
O exemplo dispara o erro sem tratamento em um processo filho, para mostrar
o código de saída real sem matar o exemplo.
try { divide(10, 0); } catch (erro) { console.error(erro.name + ': ' + erro.message); // stderr: o terminal console.log(' erro tratado:', erro.code, '-', erro.message); }
O par das duas linhas é o padrão do material: o console.error mostra o erro
para quem está olhando o terminal, e o console.log é o que sobrevive na
página, porque a página embute o stdout.
Exemplo
'use strict'; // Exemplo da aula 2 do dia 1: rodar o arquivo, ver os argumentos e o codigo de // saida. // // Um programa de terminal responde a tres perguntas: com qual nome eu fui // chamado, o que recebi de argumento e como eu termino. As tres tem resposta // em `process`. console.log('executavel:', process.execPath); console.log('diretorio atual:', process.cwd()); // `argv[0]` e o proprio node, `argv[1]` e o caminho do arquivo. O que vem depois // disso sao os argumentos de quem chamou: `node app.js 42 --semente`. console.log('process.argv inteiro:', JSON.stringify(process.argv)); const argumentos = process.argv.slice(2); console.log('argumentos do usuario:', argumentos.length ? argumentos.join(' ') : '(nenhum)'); // O outro lado da linha de comando e a variavel de ambiente: nao aparece no // `ps` de quem olha de fora, e por isso e onde mora o segredo. console.log('NODE_ENV:', process.env.NODE_ENV || '(nao definido)'); // --- codigo de saida, com erro tratado --- function divide(a, b) { if (b === 0) { const erro = new Error('divisao por zero'); erro.code = 'ER_DIVISAO'; throw erro; } return a / b; } console.log(''); console.log('10 / 2 =', divide(10, 2)); console.log('agora 10 / 0, dentro de try:'); try { divide(10, 0); console.log(' nao chega aqui'); } catch (erro) { console.error(erro.name + ': ' + erro.message); // stderr: so o terminal console.log(' erro tratado:', erro.code, '-', erro.message); console.log(' primeira linha do stack:', erro.stack.split('\n')[1].trim()); } // Erro NAO tratado e o outro caso: o processo morre com codigo 1 e imprime o // stack inteiro no stderr. Para mostrar isso sem matar este exemplo, o erro // acontece em um processo FILHO e o pai le o que o filho imprimiu. console.log(''); console.log('processo filho com erro nao tratado:'); const { execFileSync } = require('node:child_process'); try { execFileSync( process.execPath, ['-e', 'const vazio = null; console.log(vazio.naoExiste);'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] } ); } catch (falha) { const linhas = String(falha.stderr).trim().split('\n'); console.log(' codigo de saida do filho:', falha.status); console.log(' primeira linha:', linhas[0]); console.log(' origem:', linhas.slice(1).find((l) => l.trim().startsWith('at ')).trim()); console.log(' no pai, o processo continua vivo'); } // `process.exitCode` diz como terminar sem matar no meio: o Node sai sozinho // quando nao ha mais nada na fila, e o shell le este valor no `$?`. process.exitCode = 0; console.log(''); console.log('este processo vai sair com exitCode', process.exitCode);
Saída real
executavel: /root/.hermes/node/bin/node diretorio atual: /root/materiais/node-mysql process.argv inteiro: ["/root/.hermes/node/bin/node","/root/materiais/node-mysql/codigo/t2/dia01/aula2.js"] argumentos do usuario: (nenhum) NODE_ENV: (nao definido) 10 / 2 = 5 agora 10 / 0, dentro de try: erro tratado: ER_DIVISAO - divisao por zero primeira linha do stack: at divide (/root/materiais/node-mysql/codigo/t2/dia01/aula2.js:26:18) processo filho com erro nao tratado: codigo de saida do filho: 1 primeira linha: [eval]:1 origem: at [eval]:1:39 no pai, o processo continua vivo este processo vai sair com exitCode 0