Por que o Node.js é importante
O Node.js é um runtime que executa JavaScript fora do navegador. Lançado em 2009, ele utilizou o motor V8 do Chrome e o combinou com APIs para arquivos, rede e processos, permitindo que desenvolvedores utilizem a mesma linguagem em todo o stack.
Sua característica definidora é o I/O não bloqueante e orientado a eventos. Em vez de reservar uma thread por requisição e esperar enquanto o disco ou a rede respondem, o Node delega a operação lenta e continua com outras tarefas. Quando a operação termina, um callback é executado. Esse modelo o torna eficiente para APIs, apps em tempo real e ferramentas, sendo o mesmo event loop que você já conhece no JavaScript do navegador.
Executando JavaScript no servidor
O Node oferece a você um process global, um sistema de módulos e uma biblioteca padrão. Um servidor HTTP minimalista não precisa de dependências.
// server.js
import { createServer } from "node:http";
const server = createServer((req, res) => {
if (req.url === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok" }));
return;
}
res.writeHead(404).end("Not found");
});
server.listen(process.env.PORT ?? 3000);
As importações integradas com o prefixo node: deixam claro quais módulos vêm do próprio Node. Este servidor processa as requisições conforme elas chegam e nunca bloqueia o I/O.
Módulos
O Node moderno utiliza ES modules, a mesma sintaxe de import e export utilizada no navegador.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from "./math.js";
Defina "type": "module" no package.json para tratar arquivos .js como ES modules. O Node também ainda oferece suporte ao CommonJS (require e module.exports), que você encontrará em códigos mais antigos e em muitos pacotes. Novos projetos devem utilizar ES modules.
O sistema de arquivos
A API de sistema de arquivos do Node possui variantes assíncronas e síncronas. Use a API assíncrona em servidores.
// files.js
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
const file = path.join(import.meta.dirname, "data.json");
const data = JSON.parse(await readFile(file, "utf8"));
await writeFile(file, JSON.stringify({ ...data, updated: Date.now() }, null, 2));
O módulo node:path lida com a junção de caminhos entre diferentes plataformas, e import.meta.dirname fornece o diretório do arquivo atual. Funções síncronas como readFileSync existem e são aceitáveis em scripts e códigos de inicialização, mas nunca dentro de um request handler.
Ambiente e configuração
A configuração deve ficar em variáveis de ambiente, não no código.
// env.js
const config = {
port: Number(process.env.PORT ?? 3000),
databaseUrl: process.env.DATABASE_URL,
nodeEnv: process.env.NODE_ENV ?? "development",
};
Isso mantém segredos fora do repositório e permite que a mesma build seja executada em desenvolvimento, staging e produção. Carregue valores locais de um arquivo .env em desenvolvimento com uma ferramenta como dotenv ou o --env-file nativo do Node.js, e nunca faça commit desse arquivo.
Construindo APIs
A maioria dos apps Node.js utiliza um framework para lidar com roteamento, parsing e middleware. O Express é o mais utilizado; o Fastify é mais rápido e orientado a schemas.
// api.js
import express from "express";
const app = express();
app.use(express.json());
app.get("/api/users", async (req, res, next) => {
try {
const users = await db.users.findMany();
res.json(users);
} catch (error) {
next(error);
}
});
app.post("/api/users", async (req, res, next) => {
try {
const user = await db.users.create(req.body);
res.status(201).json(user);
} catch (error) {
next(error);
}
});
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({ message: "Something went wrong" });
});
app.listen(config.port);
Os handlers de rota são funções async, os erros são encaminhados para o middleware e um handler de erro final transforma exceções em respostas. Retorne status codes e headers significativos, e consulte Segurança Web para validar inputs e proteger sessões.
Streams, buffers e events
O Node manipula dados como streams para que payloads grandes não precisem caber inteiramente na memória.
// stream.js
import { createReadStream } from "node:fs";
import { createServer } from "node:http";
createServer((req, res) => {
createReadStream("movie.mp4").pipe(res);
}).listen(3000);
Um buffer consiste em dados binários brutos, e um event emitter permite que objetos publiquem eventos nos quais outros se inscrevem. Streams e events são a engrenagem de baixo nível por trás de grande parte da biblioteca padrão do Node, e entendê-los explica como arquivos, sockets e HTTP funcionam internamente.
Erros e sinais de processo
Erros não tratados podem derrubar um processo Node.js, portanto, trate-os de forma deliberada.
// signals.js
process.on("SIGTERM", () => {
server.close(() => process.exit(0));
});
process.on("unhandledRejection", (reason) => {
console.error("Unhandled rejection", reason);
});
SIGTERM é enviado pela maioria das plataformas quando um container ou processo deve ser interrompido; tratá-lo permite que você finalize as requisições em andamento antes de encerrar a execução. Handlers globais servem como uma rede de segurança para logging, e não como um substituto para o tratamento de erros onde eles ocorrem.
Melhores práticas
- Use APIs async nos request handlers e nunca bloqueie o event loop.
- Mantenha as configurações em variáveis de ambiente.
- Valide e sanitize todas as entradas no servidor.
- Use ES modules e o prefixo
node:para built-ins. - Centralize o tratamento de erros em middleware.
- Faça stream de arquivos grandes em vez de lê-los na memória.
- Trate
SIGTERMpara um graceful shutdown e exponha um endpoint de health.
Erros comuns
- Usar
readFileSyncdentro de um request handler e travar o servidor. - Executar tarefas pesadas de CPU na main thread.
- Commitar arquivos
.envou deixar secrets hardcoded. - Confiar em inputs do cliente sem validação.
- Ignorar unhandled promise rejections.
- Ler arquivos grandes inteiramente na memória em vez de usar streaming.
Próximos passos
O Node.js completa a visão full-stack. Gerencie dependências com npm, entenda o protocolo HTTP que seu servidor utiliza, proteja-o com Web Security e empacote-o com Docker. Depois, construa uma pequena API com duas rotas, uma chamada de banco de dados e um manipulador de erros, e faça o deploy.