Por que a manipulação de arquivos é importante
Servidores leem e escrevem arquivos constantemente: configurações, uploads, logs, caches, relatórios gerados e outputs de build. O módulo node:fs do Node.js é a interface, e a forma como você o utiliza decide se o seu servidor permanecerá responsivo e se os seus dados estarão seguros.
Três pontos são fundamentais. Use a promise API para que o event loop permaneça livre. Construa paths de forma segura para que a entrada do usuário não consiga escapar de um diretório. E utilize streams para arquivos grandes, evitando que o consumo de memória cresça proporcionalmente ao tamanho do arquivo.
Lendo e escrevendo arquivos
A API de promises é o padrão para o código da aplicação.
// files.js
import { readFile, writeFile, appendFile } from "node:fs/promises";
// text
const text = await readFile("notes.md", "utf8");
await writeFile("notes.md", text + "\nnew line\n", "utf8");
await appendFile("app.log", "event\n", "utf8");
// binary
const image = await readFile("photo.png"); // Buffer
await writeFile("copy.png", image);
Passe uma codificação como "utf8" para obter uma string; omita-a para obter um Buffer de bytes brutos. writeFile substitui o arquivo, enquanto appendFile adiciona conteúdo a ele. Nenhum dos dois cria diretórios inexistentes, portanto, chame mkdir primeiro quando necessário.
Diretórios
// dirs.js
import { mkdir, readdir, rm, rename, stat } from "node:fs/promises";
await mkdir("data/cache", { recursive: true });
const entries = await readdir("data", { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) console.log("dir", entry.name);
}
const info = await stat("data/cache");
console.log(info.size, info.mtime);
await rename("data/tmp.json", "data/final.json");
await rm("data/old", { recursive: true, force: true });
recursive: true faz com que mkdir e rm operem em árvores completas, e withFileTypes retorna entradas que você pode testar sem chamadas extras de stat. Use stat para verificar existência, tamanho e timestamps, mas esteja ciente de que ele segue symlinks; lstat não segue.
Caminhos
O módulo node:path lida com a manipulação de caminhos de forma portátil.
// paths.js
import path from "node:path";
const file = path.join("uploads", "avatars", "ada.png");
const absolute = path.resolve("uploads", "ada.png");
const name = path.basename(file); // ada.png
const ext = path.extname(file); // .png
const dir = path.dirname(file); // uploads/avatars
Use path.join e path.resolve em vez de concatenação de strings, e import.meta.dirname (ou import.meta.url em versões mais antigas) para localizar arquivos relativos ao módulo atual. No Windows, path utiliza barras invertidas, e é por isso que a concatenação manual falha.
Segurança: path traversal
A vulnerabilidade de arquivos mais comum é permitir que a entrada do usuário escape do diretório pretendido.
// safe-path.js
import path from "node:path";
const BASE = "/srv/uploads";
function safePath(name) {
const target = path.resolve(BASE, name);
if (target !== BASE && !target.startsWith(BASE + path.sep)) {
throw new Error("Invalid path");
}
return target;
}
Resolva a entrada em relação a uma base fixa e, em seguida, verifique se o resultado ainda está dentro dela. Além disso, valide os nomes dos arquivos, rejeite null bytes e nunca exponha erros brutos do sistema de arquivos para os clientes. Consulte o guia de Web Security.
Escritas atômicas
Se ocorrer uma falha durante a escrita, um leitor poderá visualizar um arquivo escrito parcialmente. Para estado e configuração, escreva em um arquivo temporário e renomeie-o para o local final.
// atomic.js
import { writeFile, rename } from "node:fs/promises";
import { randomUUID } from "node:crypto";
const tmp = `/srv/state/.${randomUUID()}.tmp`;
await writeFile(tmp, JSON.stringify(state), "utf8");
await rename(tmp, "/srv/state/current.json");
rename é atômico no mesmo sistema de arquivos, portanto, os leitores sempre verão ou o arquivo antigo ou o novo completo.
Streaming de arquivos grandes
Para qualquer conteúdo que possa ser volumoso, utilize stream em vez de buffering.
// stream.js
import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
await pipeline(
createReadStream("big.csv"),
createWriteStream("copy.csv"),
);
O streaming mantém o uso de memória estável e permite que você comece o processamento antes mesmo que o arquivo inteiro seja lido. O guia de Streams aborda transforms, backpressure e object mode.
Monitorando arquivos
// watch.js
import { watch } from "node:fs";
const watcher = watch("./config", { recursive: true }, (event, filename) => {
console.log(event, filename);
});
process.on("SIGINT", () => watcher.close());
Watchers são ideais para servidores de desenvolvimento e recarregamento de configurações. Utilize debounce para evitar rajadas de eventos, trate exclusões e renomeações, e sempre feche o watcher para que o processo possa ser encerrado.
Melhores práticas
- Use
node:fs/promisesno código da aplicação. - Leia e escreva com uma codificação (encoding) explícita, ou espere um Buffer.
- Construa caminhos com
node:pathe resolva-os em relação a uma base conhecida. - Proteja-se contra path traversal em qualquer nome de arquivo fornecido pelo usuário.
- Use streams para arquivos grandes; faça buffer apenas do que você precisar por inteiro.
- Escreva arquivos de estado atomicamente usando um arquivo temporário e renomeando-o.
- Feche watchers e file handles para que os processos possam encerrar corretamente.
Erros comuns
- Usar
readFileSyncem um request handler. - Concatenar entradas do usuário em caminhos (paths).
- Assumir que um diretório existe antes de escrever.
- Ler um arquivo enorme na memória e causar crash sob carga.
- Expor erros brutos do sistema de arquivos e vazar caminhos.
- Esquecer de fechar watchers ou streams.
Próximos passos
A manipulação de arquivos é uma tarefa diária no servidor. Aprofunde seus conhecimentos com Streams, entenda o agendamento no guia do Event Loop e torne seus caminhos mais seguros com o guia de Web Security.