Tudo que você já sabe em PHP, traduzido para o mundo Node. Com tabelas comparativas, pegadinhas e macetes.
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 |
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. ┌─────────────────────────────┐
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:
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');| 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óriaNem 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));
});
});# 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) |
{
"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// 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" };// 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)// 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// 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], ...]// 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; };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.
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";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.
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.
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)); // 6Tratamento 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);
});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:moduleTratamento 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");
}
}// 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)// 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// 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)]);// 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 fontesasync 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 });// 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");// 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
}// 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)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
PHP tem require/include/use. Node tem dois sistemas:
// 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// 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).
// 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
}
}// 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) { ... }// 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");// index.php — Apache/Nginx direciona cada request
echo json_encode(["mensagem" => "Olá!"]);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'));| 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 |
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árioprocess.env sempre retorna string. "0" é truthy em JS!
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 | — |
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 } });// 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// 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 !==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 dinheiroclass 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);
}
}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.
// 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);
});// 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 💥// 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 });// 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();
});// 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
}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
});// 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 => { ... });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)));// 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");
}// 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+)// 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);
});// 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| 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 |
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.
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
Esta é a pergunta honesta que todo dev se faz. A resposta real é: depende do problema, não de hype.
| 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 |
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
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
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
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
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.
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— useconst(preferido) oulet - Use
async/awaitem vez de callbacks ou.then()encadeado - Use
try/catchem TODO código assíncrono - Use
for...ofcomawait, nuncaforEachcomawait - Trate todas as Promises — Promise sem
.catch()outry/catchpode 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 functionsempre retorna uma Promise — nunca o valor direto - Usar
Promise.allpara operações independentes em paralelo (nãoawaitsequencial) - Usar
Promise.allSettledquando 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
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.
| 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 |
// 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.
// 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.
→ 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
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.
// 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.
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.
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
# 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)// 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 muitoQualquer 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.
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)));
}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.
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
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.