Skip to content

Instantly share code, notes, and snippets.

@borjao-dev
Last active May 17, 2026 15:40
Show Gist options
  • Select an option

  • Save borjao-dev/6cde040ad6f3867ee29663db7c4ac416 to your computer and use it in GitHub Desktop.

Select an option

Save borjao-dev/6cde040ad6f3867ee29663db7c4ac416 to your computer and use it in GitHub Desktop.
Node.js para Devs PHP — Guia Completo

Node.js para Devs PHP — Guia Completo

Tudo que você já sabe em PHP, traduzido para o mundo Node. Com tabelas comparativas, pegadinhas e macetes.


1. A diferença fundamental de modelo

Esta é a maior virada de chave. Entenda isso e o resto faz sentido.

Aspecto PHP Node.js
Modelo de execução Um processo por requisição Um único processo, todas as requisições
Concorrência Threads/processos separados pelo servidor web Event loop (single-thread, não bloqueante)
Ciclo de vida Script nasce, executa, morre Processo fica vivo indefinidamente
Estado entre requisições Zero (tudo morre) Compartilhado (variáveis globais persistem!)
I/O Bloqueante por padrão Não-bloqueante por padrão
Crash de um usuário Afeta só aquela requisição Pode derrubar o servidor inteiro
Uso de memória Liberada ao fim de cada request Acumula — memory leaks são reais
CPU intensiva O servidor web escala com mais processos Trava o event loop de todos

1.1 Como o PHP lida com requisições (modelo "share-nothing")

Requisição A → [processo PHP A] → executa → morre → limpa tudo
Requisição B → [processo PHP B] → executa → morre → limpa tudo
Requisição C → [processo PHP C] → executa → morre → limpa tudo

Cada processo é completamente isolado. Se o processo A explodir, B e C nem ficam sabendo. O Apache/Nginx/PHP-FPM cria e mata processos conforme necessário. Você nunca pensa nisso — é transparente.

Isso é o motivo pelo qual em PHP você pode fazer coisas "descuidadas" e sobreviver:

// PHP — isso é "ok" porque morre junto com a requisição
$conexao = mysqli_connect(...);
// Esqueceu de fechar? Não importa, vai morrer em 2 segundos.

// Memória vazando? Cada request começa do zero.
// Variável global suja? Próxima request não herda.

1.2 Como o Node lida com requisições (event loop)

                    ┌─────────────────────────────┐
Requisição A ──────▶│                             │
Requisição B ──────▶│   ÚNICO PROCESSO NODE.JS    │
Requisição C ──────▶│       (event loop)          │
Requisição D ──────▶│                             │
                    └─────────────────────────────└

Um único processo JS atende todas as requisições. A "mágica" é que ele nunca fica bloqueado esperando I/O — enquanto espera o banco responder para a requisição A, ele já processa a requisição B.

O event loop funciona assim:

1. Recebe Req A: "buscar usuário no banco"
   → dispara a query (não espera) → passa para o próximo

2. Recebe Req B: "retornar status 200"
   → responde imediatamente ✅

3. Banco respondeu para Req A
   → volta para Req A, finaliza, responde ✅

Isso é extremamente eficiente para I/O (banco, arquivos, HTTP externo). Mas tem um calcanhar de Aquiles mortal:


1.3 ☠️ O que TRAVA o event loop (e deve ser evitado a todo custo)

Qualquer código que ocupa a CPU por muito tempo sem pausar trava o servidor inteiro. Não existe "trava só para mim" — trava para todo mundo.

// ☠️ TRAVA TUDO — loop infinito óbvio
app.get('/ruim', (req, res) => {
  while (true) {} // servidor morreu
});

// ☠️ TRAVA TUDO — processamento pesado síncrono
app.get('/fibonacci', (req, res) => {
  const resultado = fibonacci(50); // cálculo demorado bloqueante
  res.json({ resultado });         // durante esse cálculo: nada mais roda
});

// ☠️ TRAVA TUDO — JSON enorme
app.get('/exportar', (req, res) => {
  const dados = JSON.stringify(arrayComMilhoesDeItens); // bloqueante!
  res.send(dados);
});

// ☠️ TRAVA TUDO — fs síncrono (evite em servidores!)
const conteudo = fs.readFileSync('/arquivo-enorme.txt'); // bloqueante!
// ✅ CERTO — delega ao sistema operacional (não bloqueia)
app.get('/fibonacci', async (req, res) => {
  // Opção 1: Worker Threads (para CPU intensiva)
  const { Worker } = require('worker_threads');
  // roda em thread separada, não trava o event loop

  // Opção 2: child_process
  const { execFile } = require('child_process');

  // Opção 3: cache o resultado se for fixo
});

// ✅ CERTO — fs assíncrono
const conteudo = await fs.promises.readFile('/arquivo.txt', 'utf8');

1.4 Comparativo de comportamento sob falha

Situação PHP Node.js
Erro fatal em uma requisição Só aquela morre Pode crashar o processo inteiro
exit() / die() Encerra só aquele script Mata o servidor
Memory leak Some com o fim da request Acumula até o processo ser reiniciado
Exceção não capturada 500 naquela requisição process.on('uncaughtException') ou crash
Timeout de banco Aquela requisição para Event loop continua (se I/O assíncrono)

Por isso Node em produção sempre usa um process manager:

# PM2 — o mais popular (reinicia automaticamente se crashar)
npm install -g pm2
pm2 start server.js --name "minha-api"
pm2 startup        # reinicia após reboot do servidor
pm2 logs           # logs em tempo real
pm2 monit          # monitor de CPU/memória

1.5 Quando o modelo de processo único é uma vantagem real

Nem tudo é desvantagem. O modelo único do Node brilha em:

Caso de uso Por quê Node ganha
WebSockets / tempo real Milhares de conexões abertas simultaneamente sem custo por thread
API gateway / proxy I/O puro, quase zero CPU — event loop ideal
Streaming de dados Lida nativamente com streams sem buffer completo na memória
Muitas requisições curtas Sem overhead de criar/matar processos
Servidor de chat Notificação push para N clientes é trivial
// Node lida com 10.000 conexões WebSocket no mesmo processo
// PHP precisaria de 10.000 processos — inviável
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
  ws.on('message', (msg) => {
    // broadcast para todos
    wss.clients.forEach(client => client.send(msg));
  });
});

2. Instalação e ambiente

# Instale o Node via NVM (recomendado, igual ao phpenv/phpbrew)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

nvm install --lts       # instala a versão LTS
nvm use --lts
node -v                 # verifica
npm -v                  # gerenciador de pacotes (vem junto)
Ferramenta PHP Equivalente Node
php -v node -v
Composer npm / yarn / pnpm
composer.json package.json
composer.lock package-lock.json / yarn.lock
vendor/ node_modules/
php artisan serve node server.js ou npm start
php -r "echo 'oi';" node -e "console.log('oi')"
REPL do PHP (php -a) node (digita direto no terminal)

3. package.json — o composer.json do Node

{
  "name": "meu-projeto",
  "version": "1.0.0",
  "description": "API em Node",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js",
    "test": "jest"
  },
  "dependencies": {
    "express": "^4.18.2",
    "dotenv": "^16.0.3"
  },
  "devDependencies": {
    "nodemon": "^3.0.1",
    "jest": "^29.0.0"
  }
}
npm install          # equivalente a composer install
npm install express  # equivalente a composer require
npm install -D jest  # --save-dev (só em dev)
npm run dev          # executa o script "dev"
npm run start        # ou simplesmente: npm start

4. Sintaxe básica — PHP vs Node

Variáveis e tipos

// PHP
$nome = "João";
$idade = 30;
$preco = 9.99;
$ativo = true;
$nada = null;
$lista = [1, 2, 3];
$mapa = ["chave" => "valor"];
// JavaScript / Node
const nome = "João";   // imutável (prefira sempre)
let idade = 30;        // mutável
// sem declaração de tipo
const preco = 9.99;
const ativo = true;
const nada = null;
const lista = [1, 2, 3];
const mapa = { chave: "valor" };

Strings

// PHP
$nome = "Maria";
echo "Olá, $nome!";                     // interpolação com aspas duplas
echo 'Olá, ' . $nome . '!';            // concatenação
echo strlen($nome);                     // 5
echo strtoupper($nome);                 // MARIA
echo str_replace("a", "o", $nome);     // Mario
// Node
const nome = "Maria";
console.log(`Olá, ${nome}!`);           // template literal (backtick)
console.log("Olá, " + nome + "!");      // concatenação
console.log(nome.length);               // 5
console.log(nome.toUpperCase());        // MARIA
console.log(nome.replace("a", "o"));    // Mario (só primeira ocorrência!)
console.log(nome.replaceAll("a", "o")); // Mario (todas)

Arrays

// PHP
$frutas = ["maçã", "banana", "uva"];
$frutas[] = "mango";                    // push
array_push($frutas, "pera");
$ultimo = array_pop($frutas);
$qtd = count($frutas);
$filtradas = array_filter($frutas, fn($f) => strlen($f) > 4);
$maiusculas = array_map(fn($f) => strtoupper($f), $frutas);
$total = array_reduce([1,2,3], fn($carry, $i) => $carry + $i, 0);
// Node
const frutas = ["maçã", "banana", "uva"];
frutas.push("mango");                   // push (mutável!)
const ultimo = frutas.pop();
const qtd = frutas.length;              // propriedade, não função!
const filtradas = frutas.filter(f => f.length > 4);
const maiusculas = frutas.map(f => f.toUpperCase());
const total = [1,2,3].reduce((acc, i) => acc + i, 0);

// Extras úteis
frutas.includes("banana");              // true
frutas.find(f => f.startsWith("m"));   // "maçã"
frutas.findIndex(f => f === "uva");    // 2
frutas.some(f => f.length > 5);        // true
frutas.every(f => f.length > 2);       // true

Objetos / Arrays Associativos

// PHP
$user = [
    "nome" => "Ana",
    "idade" => 25,
    "endereco" => ["cidade" => "SP"]
];
echo $user["nome"];
echo $user["endereco"]["cidade"];
$user["email"] = "ana@ex.com";       // adiciona
unset($user["idade"]);               // remove
isset($user["nome"]);                // true
$keys = array_keys($user);
$values = array_values($user);
// Node
const user = {
  nome: "Ana",
  idade: 25,
  endereco: { cidade: "SP" }
};
console.log(user.nome);              // notação de ponto (preferida)
console.log(user["nome"]);           // notação de colchete (também funciona)
console.log(user.endereco.cidade);
user.email = "ana@ex.com";           // adiciona
delete user.idade;                   // remove
"nome" in user;                      // true
user.nome !== undefined;             // outra forma
const keys = Object.keys(user);
const values = Object.values(user);
const entries = Object.entries(user); // [[chave, valor], ...]

5. Funções

// PHP — funções nomeadas
function soma($a, $b) { return $a + $b; }

// PHP — arrow functions (7.4+)
$dobro = fn($x) => $x * 2;

// PHP — closures
$mult = function($a, $b) { return $a * $b; };
// Node — funções nomeadas
function soma(a, b) { return a + b; }

// Node — arrow functions (ES6+)
const dobro = x => x * 2;
const soma2 = (a, b) => a + b;
const complexa = (a, b) => {
  const result = a + b;
  return result * 2;   // com chaves, precisa de return explícito
};

// Node — function expression
const mult = function(a, b) { return a * b; };

⚠️ Pegadinha — this em arrow functions: Arrow functions não têm this próprio. Herde do escopo externo. Nunca use arrow function como método de classe/objeto se precisar de this.


6. Controle de fluxo

Praticamente igual ao PHP — if/else, switch, for, while são idênticos em sintaxe.

// Destruturação — não existe em PHP (exceto list())
const [primeiro, segundo, ...resto] = [1, 2, 3, 4, 5];
// primeiro = 1, segundo = 2, resto = [3,4,5]

const { nome, idade, ...outros } = user;
// nome = "Ana", idade = 25, outros = { email: ... }

// Spread operator
const novoArray = [...frutas, "kiwi"];
const novoObj = { ...user, role: "admin" };

// Nullish coalescing (PHP 8+ tem ?? também!)
const label = user.apelido ?? "Sem apelido";

// Optional chaining (PHP 8+ tem ?-> também!)
const cidade = user?.endereco?.cidade ?? "Desconhecida";

7. O coração do Node: Assincronismo e Promises (do zero ao avançado)

Esta é a maior diferença prática. Em PHP você faz:

// PHP — tudo bloqueante, sequencial, simples de ler
$dados = file_get_contents("https://api.exemplo.com/users");
$users = json_decode($dados, true);
foreach ($users as $user) {
    echo $user["nome"] . "\n";
}
echo "Feito!";

Cada linha espera a anterior terminar. Simples e previsível. Em Node isso seria um crime — travar esperando I/O bloqueia o servidor inteiro. A solução é o modelo assíncrono, que evoluiu em três gerações.


7.1 Geração 1: Callbacks (forma antiga — evite, mas entenda)

A ideia: "não espere — me avise quando terminar".

// Em vez de: const dados = buscar(); // bloqueante
// Você passa uma função a ser chamada quando terminar:
buscar(function(err, dados) {
  // sou chamado de volta quando termina (callback)
  if (err) return tratarErro(err);
  usar(dados);
});

O problema surge quando você precisa encadear operações:

// "Callback Hell" — pirâmide da morte
buscarUsuario(id, function(err, user) {
  if (err) return cb(err);
  buscarPedidos(user.id, function(err, pedidos) {
    if (err) return cb(err);
    buscarProdutos(pedidos[0].id, function(err, produtos) {
      if (err) return cb(err);
      calcularFrete(produtos, function(err, frete) {
        if (err) return cb(err);
        // já estamos 5 níveis dentro... isso piora
        res.json({ user, pedidos, produtos, frete });
      });
    });
  });
});

Callbacks ainda aparecem em código legado e em alguns módulos nativos do Node. Você precisa reconhecê-los.


7.2 Geração 2: Promises — entendendo de vez

Uma Promise é um objeto que representa uma operação que ainda não terminou, mas vai terminar em algum momento (com sucesso ou falha).

Pense assim: é como pegar uma senha de atendimento na padaria. Você não fica parado na frente do balcão esperando — você pega a senha (Promise), senta, faz outra coisa, e quando chamarem o número, você age.

Estado da Promise:
┌─────────────┐
│   pending   │  ← acaba de ser criada, operação em andamento
└──────┬──────┘
       │
   ┌───┴───┐
   │       │
   ▼       ▼
┌──────┐ ┌──────────┐
│fulfilled│ │ rejected │  ← estados finais, imutáveis
└──────┘ └──────────┘
  .then()   .catch()

Criando uma Promise manualmente (para entender a mecânica):

// O construtor recebe uma função com dois callbacks: resolve e reject
const minhaPromise = new Promise((resolve, reject) => {
  // código assíncrono aqui...
  setTimeout(() => {
    const sucesso = true;
    if (sucesso) {
      resolve("Dados prontos!"); // resolve = "deu certo, aqui o resultado"
    } else {
      reject(new Error("Algo deu errado")); // reject = "falhou, aqui o erro"
    }
  }, 2000);
});

// A promise está criada mas ninguém está ouvindo ainda.
// Para consumir:
minhaPromise
  .then(resultado => console.log(resultado)) // "Dados prontos!"
  .catch(err => console.error(err.message));

Na prática você raramente cria Promises manualmente — você as recebe de funções assíncronas:

// fetch() retorna uma Promise automaticamente
const promiseDaResposta = fetch('https://api.exemplo.com/users');
// promiseDaResposta é uma Promise<Response>

// .then() recebe o valor resolvido e retorna OUTRA Promise
const promiseDosUsers = promiseDaResposta.then(res => res.json());
// promiseDosUsers é uma Promise<User[]>

// Encadeando (o jeito bonito):
fetch('https://api.exemplo.com/users')
  .then(res => res.json())        // Promise<User[]>
  .then(users => {                // users = User[]
    users.forEach(u => console.log(u.nome));
    return users.length;          // você pode retornar outro valor
  })
  .then(total => console.log(`Total: ${total}`))
  .catch(err => console.error("Erro:", err)) // captura qualquer erro da cadeia
  .finally(() => console.log("Feito!"));     // sempre executa (como finally do try/catch)

A regra de ouro do .then(): o que você retorna dentro de .then() vira o valor resolvido da próxima Promise na cadeia.

Promise.resolve(1)
  .then(n => n + 1)     // retorna 2
  .then(n => n * 3)     // retorna 6
  .then(n => console.log(n)); // 6

Tratamento de erros com Promises:

// .catch() captura rejeições de qualquer ponto da cadeia acima
fetch('/api/dados')
  .then(res => {
    if (!res.ok) throw new Error(`Erro HTTP: ${res.status}`); // joga para o catch
    return res.json();
  })
  .then(dados => processar(dados))
  .then(resultado => salvar(resultado))
  .catch(err => {
    // qualquer erro de qualquer .then() acima chega aqui
    console.error("Falhou em algum ponto:", err.message);
  });

7.3 Geração 3: Async/Await — Promises com cara de PHP

async/await não é uma coisa diferente de Promises — é açúcar sintático por cima delas. Por baixo dos panos, é exatamente a mesma coisa. Mas fica muito mais legível.

// Com Promise pura:
function buscarUser(id) {
  return fetch(`/api/users/${id}`)
    .then(res => res.json())
    .then(user => {
      if (!user) throw new Error("Não encontrado");
      return user;
    });
}

// Exatamente equivalente com async/await:
async function buscarUser(id) {
  const res = await fetch(`/api/users/${id}`); // espera a Promise resolver
  const user = await res.json();               // espera outra Promise
  if (!user) throw new Error("Não encontrado");
  return user; // retorna um valor, mas async transforma em Promise automaticamente
}

async transforma qualquer função em retornadora de Promise:

async function soma(a, b) {
  return a + b; // parece retornar número, mas retorna Promise<number>
}

const resultado = soma(2, 3);
console.log(resultado);           // Promise { 5 } — não é 5!
console.log(await soma(2, 3));    // 5 ✅
soma(2, 3).then(r => console.log(r)); // 5 ✅

await só funciona dentro de async:

// ERRADO — await fora de async (exceto top-level em ES modules)
function main() {
  const dados = await buscar(); // SyntaxError!
}

// CERTO
async function main() {
  const dados = await buscar(); // OK
}

// Top-level await (funciona em ES modules com "type": "module")
const dados = await buscar(); // OK no topo do arquivo .mjs ou com type:module

Tratamento de erros com async/await:

// await + try/catch = equivalente ao .then().catch()
async function exemplo() {
  try {
    const res = await fetch('/api/dados');
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const dados = await res.json();
    return dados;
  } catch (err) {
    // captura tanto erros de rede quanto o throw manual acima
    console.error("Erro:", err.message);
    return null; // ou re-lança: throw err
  } finally {
    console.log("Sempre executa");
  }
}

7.4 Métodos estáticos de Promise — o arsenal completo

Promise.all — paralelo, todos devem ter sucesso

// Dispara todas simultaneamente, espera TODAS terminarem
// Se qualquer uma falhar, rejeita imediatamente (fail-fast)
const [users, produtos, config] = await Promise.all([
  buscarUsers(),    // começa agora
  buscarProdutos(), // começa agora (em paralelo!)
  buscarConfig(),   // começa agora
]);
// Tempo total = a mais lenta das três (não a soma)

Promise.allSettled — paralelo, não importa se falhar

// Espera TODAS terminarem, independente de sucesso ou falha
const resultados = await Promise.allSettled([
  buscarUsers(),
  operacaoQuePoderFalhar(),
  outraOperacao(),
]);

resultados.forEach(r => {
  if (r.status === 'fulfilled') {
    console.log("Sucesso:", r.value);
  } else {
    console.log("Falhou:", r.reason.message);
  }
});
// Útil para operações independentes onde você quer saber o que deu errado em cada uma

Promise.race — o mais rápido vence

// Resolve/rejeita com a PRIMEIRA que terminar (vencedor leva tudo)
const resultado = await Promise.race([
  buscarDaAPI(),
  timeout(5000), // rejeita após 5 segundos
]);

// Implementando timeout com race:
function timeout(ms) {
  return new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout após ${ms}ms`)), ms)
  );
}

const dados = await Promise.race([fetch('/api/lenta'), timeout(3000)]);

Promise.any — o primeiro a ter SUCESSO

// Resolve com o primeiro que tiver sucesso (ignora falhas)
// Só rejeita se TODOS falharem
const resultado = await Promise.any([
  buscarDoServidor1(),
  buscarDoServidor2(), // fallback
  buscarDoServidor3(), // fallback do fallback
]);
// Útil para redundância / múltiplas fontes

7.5 Armadilhas clássicas com Promises e async/await

Esquecer o await (o erro mais comum)

async function salvar(dados) {
  await db.insert(dados); // grava no banco
}

// ERRADO — não espera o banco gravar!
salvar(dados);
res.json({ ok: true }); // responde antes de gravar

// CERTO
await salvar(dados);
res.json({ ok: true });

Promise dentro de forEach (abordado na seção de pegadinhas, mas vale repetir)

// ERRADO — forEach não sabe esperar Promises
[1, 2, 3].forEach(async (id) => {
  await processar(id); // dispara as 3 simultaneamente e não espera nenhuma
});
console.log("Feito"); // imprime ANTES de qualquer processar() terminar

// CERTO — sequencial
for (const id of [1, 2, 3]) {
  await processar(id);
}
console.log("Feito"); // imprime DEPOIS de todas

// CERTO — paralelo (mais rápido)
await Promise.all([1, 2, 3].map(id => processar(id)));
console.log("Feito");

Criar Promises desnecessárias (antipadrão)

// ERRADO — Promise wrapping Promise (redundante e confuso)
async function buscar(id) {
  return new Promise(async (resolve, reject) => { // nunca faça isso
    try {
      const user = await db.find(id);
      resolve(user);
    } catch(e) {
      reject(e);
    }
  });
}

// CERTO — async/await já retorna Promise automaticamente
async function buscar(id) {
  return db.find(id); // simples
}

Encadear em vez de aguardar em paralelo

// LENTO — sequencial sem necessidade (cada um espera o anterior)
const users    = await buscarUsers();    // 1s
const produtos = await buscarProdutos(); // mais 1s
const config   = await buscarConfig();   // mais 1s
// Total: ~3 segundos

// RÁPIDO — paralelo quando não há dependência entre eles
const [users, produtos, config] = await Promise.all([
  buscarUsers(),
  buscarProdutos(),
  buscarConfig(),
]);
// Total: ~1 segundo (o mais lento dos três)

7.6 Resumo visual: Promises em uma página

Criação          Estado          Consumo
─────────        ──────          ───────

fetch(url)   →  pending    →  .then(valor => ...)
new Promise  →  fulfilled  →  .catch(err => ...)
async fn()   →  rejected   →  .finally(() => ...)
                            →  await (dentro de async)

Combinação de múltiplas:
Promise.all([p1,p2,p3])         → espera todas, falha rápido
Promise.allSettled([p1,p2,p3])  → espera todas, nunca falha
Promise.race([p1,p2,p3])        → primeiro a terminar (sucesso ou falha)
Promise.any([p1,p2,p3])         → primeiro a ter sucesso

8. Módulos — require vs import

PHP tem require/include/use. Node tem dois sistemas:

CommonJS (padrão antigo, ainda muito usado)

// exportar
// utils.js
function saudacao(nome) { return `Olá, ${nome}!`; }
const PI = 3.14159;
module.exports = { saudacao, PI };

// importar
const { saudacao, PI } = require('./utils');
const fs = require('fs');           // módulo nativo
const express = require('express'); // pacote npm

ES Modules (padrão moderno)

// Ative no package.json: "type": "module"
// ou use a extensão .mjs

// exportar — utils.js
export function saudacao(nome) { return `Olá, ${nome}!`; }
export const PI = 3.14159;
export default class MinhaClasse {}  // export padrão

// importar
import { saudacao, PI } from './utils.js';  // extensão obrigatória!
import MinhaClasse from './utils.js';
import * as utils from './utils.js';

Regra prática: Use CommonJS (require) em projetos legados/backend puro. Use ES Modules (import) em projetos novos ou com bundlers (Vite, Webpack).


9. Tratamento de erros

// PHP
try {
    $result = divisao(10, 0);
} catch (DivisionByZeroError $e) {
    echo "Erro: " . $e->getMessage();
} finally {
    echo "Sempre executa";
}
// Node — síncrono
try {
  const result = divisao(10, 0);
} catch (err) {
  console.error("Erro:", err.message);
} finally {
  console.log("Sempre executa");
}

// Node — assíncrono (obrigatório usar try/catch com async/await)
async function exemplo() {
  try {
    const data = await fetch('/api/dados');
    if (!data.ok) throw new Error(`HTTP ${data.status}`);
    return await data.json();
  } catch (err) {
    console.error("Falhou:", err.message);
    throw err; // re-lança se necessário
  }
}

⚠️ Pegadinha: Promise não capturada

// ERRADO — erro silencioso (ou crash em Node moderno)
async function mal() {
  throw new Error("Ops!");
}
mal(); // Promise rejeitada, ninguém ouve

// CERTO
mal().catch(err => console.error(err));
// ou
try { await mal(); } catch(e) { ... }

10. Classes e OOP

// PHP
class Animal {
    protected string $nome;

    public function __construct(string $nome) {
        $this->nome = $nome;
    }

    public function falar(): string {
        return "...";
    }
}

class Cachorro extends Animal {
    public function falar(): string {
        return "Au! Sou {$this->nome}";
    }
}

$rex = new Cachorro("Rex");
echo $rex->falar();
// Node
class Animal {
  #nome; // campo privado (# = privado de verdade)

  constructor(nome) {
    this.#nome = nome;
  }

  get nome() { return this.#nome; } // getter

  falar() {
    return "...";
  }
}

class Cachorro extends Animal {
  constructor(nome) {
    super(nome); // obrigatório antes de usar this
  }

  falar() {
    return `Au! Sou ${this.nome}`;
  }

  static criar(nome) { // método estático
    return new Cachorro(nome);
  }
}

const rex = new Cachorro("Rex");
console.log(rex.falar());
const bolt = Cachorro.criar("Bolt");

11. Servidor HTTP — Express vs PHP puro / Laravel

PHP puro

// index.php — Apache/Nginx direciona cada request
echo json_encode(["mensagem" => "Olá!"]);

Node com Express (framework mais popular)

npm install express
// server.js
const express = require('express');
const app = express();

app.use(express.json()); // parser de body JSON (equivalente ao php://input)

// Rota GET
app.get('/', (req, res) => {
  res.json({ mensagem: "Olá!" });
});

// Parâmetros de rota (:id = $_GET no PHP)
app.get('/users/:id', async (req, res) => {
  const { id } = req.params;       // req.params = parâmetros de rota
  const { page } = req.query;      // req.query  = query string (?page=2)
  // req.body                       // req.body   = corpo POST/PUT
  // req.headers                    // req.headers = cabeçalhos

  try {
    const user = await db.findUser(id);
    if (!user) return res.status(404).json({ erro: "Não encontrado" });
    res.json(user);
  } catch (err) {
    res.status(500).json({ erro: err.message });
  }
});

// POST
app.post('/users', async (req, res) => {
  const { nome, email } = req.body;
  const user = await db.createUser({ nome, email });
  res.status(201).json(user);
});

app.listen(3000, () => console.log('Servidor em http://localhost:3000'));

Comparativo de equivalências

PHP / Laravel Node / Express
$_GET['id'] req.query.id
$_POST['nome'] req.body.nome
$_SERVER['HTTP_*'] req.headers['*']
Route::get('/path', fn) app.get('/path', fn)
Middleware Laravel app.use(fn) ou middleware de rota
response()->json() res.json()
response()->status(404) res.status(404)
dd() / dump() console.log()
.env + env('KEY') .env + process.env.KEY

12. Variáveis de ambiente

npm install dotenv
# .env
DB_HOST=localhost
DB_PORT=5432
APP_SECRET=minhasenha123
// No início do app (index.js)
require('dotenv').config(); // ou: import 'dotenv/config'

const host = process.env.DB_HOST;     // "localhost"
const porta = process.env.DB_PORT;    // "5432" — SEMPRE STRING!
const portaNum = parseInt(process.env.DB_PORT); // converta quando necessário

⚠️ Pegadinha: process.env sempre retorna string. "0" é truthy em JS!


13. Banco de dados

PHP tem PDO. Node tem várias opções:

Abordagem Biblioteca Equivalente PHP
Query Builder Knex.js PDO
ORM completo Prisma Eloquent (Laravel)
ORM clássico Sequelize Doctrine
Mongo Mongoose

Exemplo com Prisma (recomendado para novos projetos)

npm install prisma @prisma/client
npx prisma init
// prisma/schema.prisma
model User {
  id        Int      @id @default(autoincrement())
  nome      String
  email     String   @unique
  criadoEm DateTime @default(now())
}
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

// Buscar todos
const users = await prisma.user.findMany();

// Buscar um
const user = await prisma.user.findUnique({ where: { id: 1 } });

// Criar
const novo = await prisma.user.create({
  data: { nome: "Ana", email: "ana@ex.com" }
});

// Atualizar
await prisma.user.update({
  where: { id: 1 },
  data: { nome: "Ana Silva" }
});

// Deletar
await prisma.user.delete({ where: { id: 1 } });

14. Sistema de arquivos

// PHP
$conteudo = file_get_contents('/tmp/arquivo.txt');
file_put_contents('/tmp/arquivo.txt', 'novo conteúdo');
$linhas = file('/tmp/arquivo.txt');
// Node — módulo nativo 'fs'
const fs = require('fs/promises'); // versão com Promise (prefira esta)

// Ler
const conteudo = await fs.readFile('/tmp/arquivo.txt', 'utf8');

// Escrever
await fs.writeFile('/tmp/arquivo.txt', 'novo conteúdo');

// Ler em linhas
const linhas = conteudo.split('\n');

// Append
await fs.appendFile('/tmp/arquivo.txt', '\nNova linha');

// Verificar se existe
try {
  await fs.access('/tmp/arquivo.txt');
  console.log("existe");
} catch {
  console.log("não existe");
}

// Listar diretório
const arquivos = await fs.readdir('/tmp');

// Criar diretório
await fs.mkdir('/tmp/nova-pasta', { recursive: true }); // como mkdir -p

15. Principais macetes e pegadinhas

⚠️ 1. == vs ===

// PHP tem == e === também, mas JS é mais traiçoeiro
0 == false    // true  (comparação fraca)
0 === false   // false (comparação estrita — USE SEMPRE ESTA)
"" == false   // true
null == undefined // true
null === undefined // false

// Regra: USE SEMPRE === e !==

⚠️ 2. Tipos numéricos — sem int separado

typeof 42       // "number"
typeof 42.5     // "number"  — não existe separação int/float!

// Problema clássico:
console.log(0.1 + 0.2);  // 0.30000000000000004 (IEEE 754)
// Use toFixed() para exibição, ou bibliotecas como decimal.js para dinheiro

⚠️ 3. this se perde fora do contexto

class Timer {
  constructor() { this.count = 0; }

  iniciar() {
    // ERRADO: this será undefined dentro do callback
    setInterval(function() {
      this.count++; // 💥 TypeError
    }, 1000);

    // CERTO: arrow function herda o this
    setInterval(() => {
      this.count++; // ✅
    }, 1000);
  }
}

⚠️ 4. Variáveis globais "acidentais" — o maior perigo para quem vem do PHP

Esta é provavelmente a categoria de bug mais traiçoeira na transição. Em PHP você nunca pensou nisso porque cada request é um universo isolado. Em Node, tudo que fica fora do escopo de uma função de requisição persiste para sempre, compartilhado entre todos os usuários simultâneos.

Caso 1 — Estado global simples

// ERRADO: carrinho compartilhado entre TODOS os usuários
let carrinho = [];

app.post('/adicionar', (req, res) => {
  carrinho.push(req.body.produto); // usuário A e B compartilham o mesmo array!
  res.json(carrinho);
});

// CERTO: estado vem do cliente (sessão, token, body) — nunca de variável global
app.post('/adicionar', (req, res) => {
  const carrinho = req.session.carrinho ?? [];
  carrinho.push(req.body.produto);
  req.session.carrinho = carrinho;
  res.json(carrinho);
});

Caso 2 — Módulo com estado (muito comum e muito perigoso)

// auth.js — ARMADILHA CLÁSSICA
let usuarioAtual = null; // parece inofensivo...

function login(user)    { usuarioAtual = user; } // armazenado globalmente!
function getUsuario()   { return usuarioAtual; }

module.exports = { login, getUsuario };
// server.js
app.post('/login', (req, res) => {
  auth.login(req.body.user); // seta globalmente
  res.json({ ok: true });
});

app.get('/perfil', (req, res) => {
  const user = auth.getUsuario(); // retorna o ULTIMO que logou — qualquer usuário!
  res.json(user); // bug de segurança gravíssimo
});

// Cenário real do desastre:
// Usuário A faz login   → usuarioAtual = "Ana"
// Usuário B faz login   → usuarioAtual = "Bob"  (sobrescreveu!)
// Usuário A acessa /perfil → recebe dados do Bob  💥

Caso 3 — Mutação de objetos importados

// config.js
module.exports = { timeout: 5000, retries: 3 };

// qualquer módulo pode corromper o config para todos:
const config = require('./config');
config.timeout = 999999; // mutou — todos os outros módulos veem isso!

// CERTO: congele o objeto
module.exports = Object.freeze({ timeout: 5000, retries: 3 });
// ou use uma factory function que retorna cópia nova a cada chamada:
module.exports = () => ({ timeout: 5000, retries: 3 });

Caso 4 — Acúmulo de memória (memory leak silencioso)

// ERRADO: array global que cresce infinitamente
const logs = [];
app.use((req, res, next) => {
  logs.push({ url: req.url, time: Date.now() }); // nunca limpa!
  next();
});
// Depois de 1 semana em produção: gigabytes de RAM, servidor lento ou morto

// CERTO: limite o buffer ou use Winston/Pino para logging real
const MAX_LOGS = 1000;
const logs = [];
app.use((req, res, next) => {
  logs.push({ url: req.url, time: Date.now() });
  if (logs.length > MAX_LOGS) logs.shift(); // descarta o mais antigo
  next();
});

Caso 5 — var sem let/const vaza para o escopo global

// ERRADO: sem var/let/const vira propriedade de global (global.resultado)
function processar() {
  resultado = "dados"; // vazou!
}

// CERTO
function processar() {
  const resultado = "dados"; // escopo local, some ao fim da função
}

Regra de ouro

Global (module level)  → apenas config imutável, conexões de DB, constantes
Por requisição         → tudo que varia por usuário vai em req, res, ou escopo local
// Conexão global — OK, é singleton intencional e sem estado por usuário
const db = new PrismaClient();

// Estado por usuário — sempre local à função de rota
app.get('/dados', async (req, res) => {
  const userId = req.user.id;           // vem do token/sessão
  const dados  = await db.find(userId); // busca específica
  res.json(dados);                      // escopo local, some após resposta
});

⚠️ 5. async contamina para cima

// Se uma função usa await, ela DEVE ser async
// E quem chama uma async function recebe uma Promise
// Isso "sobe" pela call stack

async function buscar() { return await fetch(...); }

// ERRADO — isso é uma Promise, não o valor!
const dados = buscar();

// CERTO
const dados = await buscar(); // só funciona dentro de outro async
// ou
buscar().then(dados => { ... });

⚠️ 6. forEach não funciona com await

const ids = [1, 2, 3];

// ERRADO — await dentro de forEach é ignorado!
ids.forEach(async (id) => {
  await processarItem(id); // não espera!
});

// CERTO — for...of
for (const id of ids) {
  await processarItem(id); // sequencial
}

// CERTO — paralelo
await Promise.all(ids.map(id => processarItem(id)));

⚠️ 7. JSON.parse / JSON.stringify

// PHP
$obj = json_decode($json, true); // true = array associativo
$json = json_encode($array);
// Node
const obj = JSON.parse(jsonString);  // lança exceção se inválido!
const jsonString = JSON.stringify(obj);
const bonito = JSON.stringify(obj, null, 2); // indentado

// Sempre use try/catch com JSON.parse
try {
  const data = JSON.parse(input);
} catch (e) {
  console.error("JSON inválido");
}

⚠️ 8. Referências vs valores

// Primitivos são copiados por valor (igual PHP)
let a = 5;
let b = a;
b = 10;
console.log(a); // 5 ✅

// Objetos e Arrays são copiados por REFERÊNCIA
const obj1 = { nome: "Ana" };
const obj2 = obj1;
obj2.nome = "Bia";
console.log(obj1.nome); // "Bia" 😱

// Clone raso
const clone = { ...obj1 };
const cloneArray = [...array];

// Clone profundo (aninhado)
const deep = JSON.parse(JSON.stringify(obj1)); // hack clássico
const deep2 = structuredClone(obj1);           // nativo moderno (Node 17+)

⚠️ 9. Erros em Promises sem catch

// Node pode crashar ou emitir warning com Unhandled Rejection
// Sempre trate erros em Promises
process.on('unhandledRejection', (err) => {
  console.error('Promise não tratada:', err);
  process.exit(1);
});

⚠️ 10. require é cacheado

// O módulo é executado UMA VEZ e o resultado é cacheado
// Modificar o export de dentro não afeta outros imports
const config = require('./config'); // executado na primeira vez
const config2 = require('./config'); // retorna o mesmo objeto do cache

16. Ferramentas essenciais do ecossistema

Categoria Ferramenta Para que serve
Framework web Express, Fastify, Hapi Servidor HTTP
Framework fullstack Next.js, Nuxt SSR + API
ORM Prisma, Sequelize, TypeORM Banco de dados
Validação Zod, Joi, Yup Validar dados (como validator do Laravel)
Autenticação Passport.js, jsonwebtoken Auth / JWT
Upload de arquivos Multer, Formidable Como $_FILES no PHP
Filas BullMQ, Bee-Queue Como Laravel Queues
Testes Jest, Vitest, Mocha PHPUnit
HTTP Client axios, got, node-fetch Como Guzzle
Linter ESLint + Prettier PHP_CodeSniffer + PHP-CS-Fixer
Reinicialização nodemon php -S com live reload
TypeScript tsc, ts-node Tipos estáticos (como PHP tipado)
Logging Winston, Pino Monolog

17. TypeScript — fortemente recomendado

TypeScript adiciona tipagem estática ao JS, tornando a experiência muito mais próxima do PHP moderno:

npm install -D typescript ts-node @types/node
npx tsc --init
// TypeScript
interface User {
  id: number;
  nome: string;
  email: string;
  idade?: number; // opcional
}

async function buscarUser(id: number): Promise<User | null> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) return null;
  return res.json() as Promise<User>;
}

function somar(a: number, b: number): number {
  return a + b;
}

Para projetos novos, use TypeScript — detecta erros antes de rodar, autocomplete muito melhor, manutenção mais fácil.


18. Estrutura de projeto recomendada

meu-projeto/
├── src/
│   ├── index.js          # entry point (como public/index.php)
│   ├── app.js            # configuração do Express
│   ├── routes/           # rotas (como routes/ do Laravel)
│   │   ├── users.js
│   │   └── produtos.js
│   ├── controllers/      # lógica dos endpoints
│   ├── services/         # regras de negócio
│   ├── models/           # modelos de dados / Prisma
│   ├── middleware/        # middlewares Express
│   └── utils/            # helpers
├── prisma/
│   └── schema.prisma
├── tests/
├── .env
├── .env.example
├── .gitignore
└── package.json

19. PHP vs Node.js — Quando usar cada um?

Esta é a pergunta honesta que todo dev se faz. A resposta real é: depende do problema, não de hype.

19.1 Tabela geral de vantagens

Critério PHP Node.js
Curva de aprendizado Muito baixa Média (async é nova mentalidade)
Ecossistema para web Maduro, imenso (WordPress, Laravel, Symfony) Imenso e crescendo rápido
Performance em I/O Boa (PHP-FPM escala bem) Excelente (event loop)
Performance em CPU Melhor que Node single-thread Ruim (trava o evento loop)
WebSockets / tempo real Difícil (Ratchet, Swoole) Nativo e trivial
Tipagem Forte no PHP 8+ Fraca (TypeScript resolve)
Debugging Mais simples (processo isolado) Mais complexo (async stack traces)
Hospedagem barata Qualquer hostinger de R$15/mês Precisa de servidor Node rodando
Tooling (bundler, etc.) Simples Complexo (mas poderoso)
Monolito com views HTML PHP brilha (Laravel Blade, etc.) Funciona, mas não é o ponto forte
APIs REST/GraphQL Muito bom Muito bom
Microsserviços Possível, mas pesado Leve e natural
Memory leaks Quase impossíveis por design Possíveis e silenciosos
Crash isola usuários Sim, por design Não, cuidado obrigatório
Contratação de devs Mercado enorme Mercado enorme
Maturidade do runtime Décadas ~15 anos, estável

19.2 Use PHP quando...

Sites e sistemas com renderização server-side (SSR) clássica

→ CMS, blogs, portais, e-commerces tradicionais
→ WordPress, WooCommerce, Magento — ecossistema incomparável
→ Nenhuma outra stack tem tanto plugin e tema pronto

Equipe pequena ou solo, velocidade de entrega é prioridade

→ Laravel entrega autenticação, filas, email, ORM, cache, jobs
  em minutos, com documentação impecável
→ PHP é mais fácil de hospedar em qualquer lugar
→ Menor overhead mental (sem async, sem process manager)

Aplicações CRUD clássicas com acesso a banco

→ Sistemas administrativos, ERPs, intranets
→ PHP + Laravel Eloquent + Blade é extremamente produtivo
→ Sem vantagem técnica real de Node aqui

Orçamento de infraestrutura limitado

→ PHP roda em hosting compartilhado baratíssimo
→ Node precisa de processo permanente (VPS, container)
→ Diferença real de custo em escala pequena

Processamento batch / scripts

→ PHP CLI funciona perfeitamente para tarefas agendadas
→ Sem complexidade de async para scripts sequenciais simples

19.3 Use Node quando...

Aplicações em tempo real

→ Chat, notificações push, dashboards ao vivo, jogos multiplayer
→ WebSockets com dezenas de milhares de conexões simultâneas
→ Server-Sent Events (SSE)
→ Node é muito superior aqui — é o caso de uso para o qual foi criado

API que serve múltiplos clientes (mobile + web + parceiros)

→ API REST ou GraphQL pura, sem renderização HTML
→ Node com Express/Fastify é leve, rápido e natural para isso
→ TypeScript torna contratos de API explícitos e seguros

Microsserviços e arquitetura orientada a eventos

→ Serviços pequenos, sem estado, comunicando via fila (RabbitMQ, Kafka)
→ Node inicializa rápido, consome pouca memória em repouso
→ Ecosystem de streams é nativo e poderoso

BFF (Backend for Frontend) — camada de orquestração

→ Serviço que agrega dados de várias APIs e entrega ao frontend
→ I/O puro: Node faz N chamadas simultâneas com Promise.all
→ PHP faria isso de forma sequencial ou com curl_multi complicado

Tooling e DevOps scripts

→ O ecosistema JS domina: Webpack, Vite, ESLint, Prettier, Jest
→ Se o time já é JS no frontend, unifica stack
→ Node scripts são mais poderosos que shell para tarefas complexas

Stack unificada com o frontend

→ Time faz React/Vue no front e não quer trocar de contexto
→ Compartilhamento de tipos (TypeScript), validações (Zod), utils entre front e back
→ Monorepo (Turborepo, Nx) fica natural

19.4 A resposta honesta sobre performance

O debate "PHP vs Node é mais rápido" é mal colocado. O gargalo quase sempre é o banco de dados, não a linguagem.

Benchmark real de uma API típica (CRUD com banco):

PHP-FPM + MySQL:    ~8.000 req/s  (com OPcache)
Node + Express:     ~12.000 req/s
Node + Fastify:     ~30.000 req/s
Go / Rust:          ~100.000+ req/s

Mas: 8.000 req/s de PHP suporta um site com 700 milhões de pageviews/mês.
Facebook usou PHP até escala absurda. Wikipedia usa PHP hoje.

A linguagem raramente é o gargalo. O que importa é:

  • Índices no banco de dados
  • Queries N+1 eliminadas
  • Cache (Redis) bem usado
  • CDN para assets estáticos

19.5 O que não faz sentido em nenhum dos dois

CPU intensiva pesada (ML, video encoding, ciência de dados)
→ Use Python (PyTorch, NumPy, Pandas) ou Go/Rust

Sistemas embarcados ou de tempo real hard
→ Use C/C++/Rust

Scripts de sysadmin simples
→ Use Bash/Python

Análise de dados / relatórios complexos
→ Use Python + Pandas ou R

19.6 Resposta prática para a maioria dos casos

Novo projeto web com equipe PHP experiente?
→ Continue em PHP (Laravel). Não troque só por hype.

Novo projeto com requisito de tempo real (chat, live, gaming)?
→ Node. PHP vai sofrer.

API para mobile app, sem SSR?
→ Ambos funcionam. Node tem mais momentum no mercado atual.

Manutenção de sistema legado PHP?
→ PHP. Não reescreva o que funciona.

Microsserviços novos em empresa com stack JS no front?
→ Node. Reduz fricção da equipe.

Solo dev fazendo SaaS rapidamente?
→ Laravel. Ecossistema mais completo out of the box.

20. Checklist rápido de transição

Modelo mental

  • Internalizar: Node é um único processo — estado global mata a segurança
  • Qualquer variável fora de uma função de rota é compartilhada por todos os usuários
  • Código síncrono pesado trava o servidor inteiro — sempre use async para I/O
  • Em produção: sempre use PM2 ou similar para reinício automático

Código

  • Use === sempre, nunca ==
  • Nunca use var — use const (preferido) ou let
  • Use async/await em vez de callbacks ou .then() encadeado
  • Use try/catch em TODO código assíncrono
  • Use for...of com await, nunca forEach com await
  • Trate todas as Promises — Promise sem .catch() ou try/catch pode crashar o processo
  • Use structuredClone() ou spread para copiar objetos (evite mutação acidental)
  • Variáveis de ambiente são sempre strings — converta explicitamente com parseInt, parseFloat, etc.

Promises especificamente

  • Entender que async function sempre retorna uma Promise — nunca o valor direto
  • Usar Promise.all para operações independentes em paralelo (não await sequencial)
  • Usar Promise.allSettled quando quiser resultado de todas, mesmo com falhas
  • Nunca criar new Promise(async (resolve) => {...}) — antipadrão desnecessário
  • Registrar process.on('unhandledRejection') para capturar Promises esquecidas

Segurança / produção

  • Nunca guarde estado por usuário em variável global
  • Use Object.freeze() em objetos de configuração importados por módulos
  • Monitore uso de memória em produção (PM2 monit ou similar)
  • Use TypeScript em projetos novos
  • Leia sempre os stack traces — são detalhados e muito úteis

21. C# (ASP.NET Core) vs Node.js — Quem é melhor em quê?

C# com ASP.NET Core é uma das stacks mais sérias e completas para backend. A comparação com Node é interessante porque os dois são modernos, performáticos e têm async/await nativo — mas com filosofias e pontos fortes bem diferentes.


21.1 Tabela geral de critérios

Critério C# / ASP.NET Core Node.js
Tipagem Estática e forte — compilador pega erros antes de rodar Dinâmica (TypeScript ajuda, mas é opt-in e menos rígido)
Performance bruta Excelente — entre os mais rápidos do mercado Muito boa para I/O; fraca para CPU
Performance CPU intensiva Muito superior — multi-thread real, sem travar o servidor Péssima — trava o event loop
Concorrência Threads reais + async/await (o melhor dos dois mundos) Event loop single-thread + async/await
Modelo de memória Gerenciado (GC), mas previsível e tunável Gerenciado (V8 GC), mais propenso a leaks silenciosos
Ecossistema NuGet: maduro, corporativo, estável npm: imenso, mas com qualidade variável e supply chain risks
Curva de aprendizado Alta — linguagem mais verbosa, mais conceitos Média — JS já conhecido, async é o desafio
Tooling / IDE Visual Studio / Rider — os melhores IDEs do mercado VS Code — excelente, mas menos integrado
Refactoring seguro Excelente — tipos + IDE fazem refactoring confiável Arriscado sem TypeScript estrito
WebSockets / tempo real SignalR — robusto e completo ws / Socket.io — simples e popular
Suporte a gRPC Nativo e excelente (Grpc.AspNetCore) Funciona, mas mais trabalhoso
Containerização Imagens Docker maiores (runtime .NET) Imagens menores (alpine Node)
Cold start (serverless) Lento — runtime .NET demora para inicializar Muito rápido — ótimo para Lambda/Functions
Custo de hospedagem Maior (precisa de mais RAM/CPU em geral) Menor
Adoção corporativa Altíssima — bancos, governo, enterprise Alta e crescendo, mais startups/scale-ups
Stack unificada front+back Não (Blazor é nicho) Sim — mesmo JS/TS no front e back
Open source / comunidade Muito bom desde .NET Core (2016+) Nativo open source desde sempre

21.2 Onde C# / ASP.NET Core é claramente superior

Processamento CPU-intensivo no servidor

// C# — roda em thread real, event loop do resto do servidor continua intacto
app.MapGet("/fibonacci", async (int n) =>
{
    // Task.Run joga para thread pool — não bloqueia outras requisições
    var resultado = await Task.Run(() => Fibonacci(n));
    return Results.Ok(new { resultado });
});
// Node — qualquer cálculo pesado síncrono trava TUDO
app.get('/fibonacci', (req, res) => {
  const resultado = fibonacci(50); // servidor congelado durante isso
  res.json({ resultado });
  // Worker Threads existem, mas são trabalhosos de usar
});

C# tem thread pool real. Você pode fazer CPU intensiva sem cerimônia. Em Node você precisa de Worker Threads com comunicação via mensagens — funciona, mas é muito mais complexo.

Tipagem estrita sem esforço extra

// C# — o compilador garante tudo isso em tempo de compilação
public record CreateUserRequest(
    [Required] string Nome,
    [EmailAddress] string Email,
    [Range(18, 120)] int Idade
);

app.MapPost("/users", async (CreateUserRequest req, AppDbContext db) =>
{
    var user = new User { Nome = req.Nome, Email = req.Email };
    db.Users.Add(user);
    await db.SaveChangesAsync();
    return Results.Created($"/users/{user.Id}", user);
});
// Validação, serialização, documentação Swagger — tudo automático
// TypeScript — muito bom, mas ainda é verificação em tempo de compilação opt-in
// Erros de runtime ainda acontecem (any, type assertions, dados externos)
interface CreateUserRequest {
  nome: string;
  email: string;
  idade: number;
}
// Validação de runtime precisa de biblioteca extra (Zod, Joi)

Em C# o sistema de tipos é mais profundo — genéricos, variância, pattern matching, records imutáveis — tudo isso com suporte completo do compilador e IDE.

Aplicações enterprise / longa duração

→ Bancos, seguradoras, ERPs, sistemas de missão crítica
→ C# tem suporte LTS previsível (Microsoft), SLA, suporte pago
→ Refactoring de base de código grande é muito mais seguro
→ Auditoria de código estático (Roslyn Analyzers) é muito mais poderosa
→ Entity Framework Core é um ORM extremamente maduro

Performance bruta em benchmarks

TechEmpower Plaintext benchmark (referência da indústria):

ASP.NET Core (Kestrel):   ~7.000.000 req/s
Node + Fastify:            ~1.000.000 req/s
Node + Express:              ~300.000 req/s

Para I/O realista (DB + JSON):
ASP.NET Core:   ~500.000 req/s
Node + Fastify:  ~200.000 req/s

C# é significativamente mais rápido quando CPU entra na equação.
Para I/O puro (espera de banco), a diferença prática diminui bastante.

gRPC e comunicação entre serviços

// C# — gRPC é cidadão de primeira classe
// Basta definir o .proto e o C# gera tudo automaticamente
[GrpcService]
public class UserService : Users.UsersBase
{
    public override async Task<UserReply> GetUser(
        UserRequest request, ServerCallContext context)
    {
        var user = await _db.FindAsync(request.Id);
        return new UserReply { Nome = user.Nome, Email = user.Email };
    }
}

Em Node, gRPC funciona com @grpc/grpc-js, mas a integração é mais manual e menos elegante.


21.3 Onde Node.js é claramente superior

Cold start e Serverless (AWS Lambda, Azure Functions, Vercel)

AWS Lambda cold start médio:
Node.js:    ~100-300ms
C# (.NET):  ~800ms - 3s  (sem Native AOT)
C# Native AOT: ~100-200ms (mas com limitações)

Para funções serverless que escalam a zero constantemente:
Node é muito mais adequado por padrão.

Stack unificada com frontend

Time full-stack JavaScript/TypeScript:
→ Um único .tsconfig, uma lint config, um monorepo
→ Tipos compartilhados entre front e back (sem gerar código)
→ Mesmos desenvolvedores trabalham em ambas as camadas
→ Sem troca de contexto de linguagem/paradigma

Com C# no back e React no front:
→ Tipos duplicados ou gerados (NSwag, Kiota)
→ Duas linguagens, dois ecossistemas, dois mundos de tooling
→ Devs full-stack precisam dominar dois universos distintos

Startup rápido e imagem Docker pequena

# Node — imagem final minúscula
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "src/index.js"]
# Imagem final: ~80-120MB

# C# — runtime .NET é pesado
FROM mcr.microsoft.com/dotnet/aspnet:8.0
# Imagem base já: ~200MB
# Imagem final típica: ~250-350MB
# (Native AOT pode reduzir, mas com restrições)

Prototipagem e velocidade de iteração

// Node — zero boilerplate para uma API funcional
import express from 'express';
const app = express();
app.use(express.json());

app.get('/ping', (req, res) => res.json({ ok: true }));
app.listen(3000);
// 6 linhas. Funciona. Nenhuma configuração extra.
// C# Minimal API (melhorou muito no .NET 6+, mas ainda mais verboso)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// + configuração de DI, logging, middleware...
var app = builder.Build();
app.MapGet("/ping", () => Results.Ok(new { ok = true }));
app.Run();
// Mais cerimônia, mas o scaffolding do Visual Studio ajuda muito

npm ecosystem para frontend tooling

Qualquer ferramenta de frontend nasce no ecossistema Node:
Vite, Webpack, esbuild, Rollup, PostCSS, Tailwind CLI,
ESLint, Prettier, Storybook, Playwright, Cypress...

C# não participa desse ecossistema.
Projetos .NET que usam React/Vue ainda precisam do Node instalado para o frontend.

21.4 Async/Await: parecido na superfície, diferente por baixo

Ambos têm async/await, mas o modelo é bem diferente:

Aspecto C# Task/async Node Promise/async
Paralelismo real Sim — usa thread pool Não — single thread
Task.Run() Joga para outra thread Não existe equivalente real
await no top level Sim (C# 9+) Sim (ES modules)
Cancelamento CancellationToken nativo AbortController (mais limitado)
Erros não tratados Warning / exceção Pode crashar o processo
CPU intensiva Transparente com Task.Run Exige Worker Threads explícitos
// C# — paralelismo real com cancelamento limpo
public async Task<List<User>> BuscarTodosAsync(CancellationToken ct)
{
    // Roda em paralelo em threads reais
    var tasks = ids.Select(id => _db.FindAsync(id, ct));
    return (await Task.WhenAll(tasks)).ToList(); // equivalente ao Promise.all
}
// Node — paralelo só em I/O (não CPU)
async function buscarTodos(ids) {
  // "paralelo" aqui significa: dispara os awaits sem bloquear entre eles
  // mas tudo ainda roda na mesma thread
  return Promise.all(ids.map(id => db.findUser(id)));
}

21.5 Injeção de Dependência

C# tem DI nativo e obrigatório por convenção. Node não tem nada nativo — você escolhe (ou não usa).

// C# — DI é central na cultura ASP.NET Core
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddSingleton<IEmailService, SmtpEmailService>();
builder.Services.AddTransient<IReportGenerator, PdfReportGenerator>();

// Injetado automaticamente no construtor
public class UserController(IUserRepository repo, IEmailService email)
{
    public async Task<IActionResult> Create(CreateUserRequest req)
    {
        var user = await repo.CreateAsync(req);
        await email.SendWelcomeAsync(user.Email);
        return Ok(user);
    }
}
// Node — sem DI nativo; você passa dependências manualmente (mais simples, menos escalável)
// Ou usa bibliotecas: InversifyJS, Awilix, tsyringe

// Abordagem mais comum em Node: factory functions
function makeUserService(db, emailService) {
  return {
    async create(data) {
      const user = await db.users.create(data);
      await emailService.sendWelcome(user.email);
      return user;
    }
  };
}

const userService = makeUserService(prisma, smtp);

Para projetos grandes com muitas dependências, a ausência de DI nativo no Node começa a pesar.


21.6 Quando escolher cada um — resumo direto

Escolha C# / ASP.NET Core quando:

→ A empresa já tem cultura .NET / time C# formado
→ Sistema financeiro, bancário, governo, healthcare — onde compliance e auditoria importam
→ CPU intensiva é requisito (processamento de imagens, cálculos, relatórios pesados)
→ Microsserviços com gRPC e comunicação binária eficiente
→ Base de código grande que vai crescer por anos (refactoring seguro)
→ Integração com ecossistema Microsoft (Azure, Active Directory, Office 365)
→ A equipe valoriza ferramentas como Visual Studio / Rider com debugging profundo

Escolha Node.js quando:

→ Time é JavaScript/TypeScript full-stack (front + back juntos)
→ API REST/GraphQL para mobile ou SPA sem requisitos de CPU pesada
→ Serverless / funções que escalam a zero (Lambda, Vercel, Cloudflare Workers)
→ Tempo real: chat, notificações, streaming com WebSockets
→ Prototipagem rápida, MVP, startup que precisa iterar veloz
→ BFF (Backend for Frontend) orquestrando múltiplas APIs
→ Equipe pequena que não quer dividir contexto entre duas linguagens

Use os dois quando:

→ Microsserviços: Node para APIs leves e BFF; C# para serviços CPU-intensivos
→ Muito comum em empresas que crescem: começa em Node, extrai serviços C# conforme necessário

21.7 A frase honesta de encerramento

PHP é o melhor quando você quer entregar rápido com pouca infra e equipe pequena. Node é o melhor quando o time é JS full-stack ou o requisito é tempo real. C# é o melhor quando performance, tipagem rígida e manutenibilidade de longo prazo são inegociáveis — especialmente com CPU intensiva ou em ambientes enterprise.

Nenhum dos três é ruim. A escolha errada é a que ignora o contexto do time e do problema.


Boa sorte na transição! O ecossistema Node é vasto, mas com essa base você já consegue construir APIs sólidas e evitar os erros que derrubam servidores em produção.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment