Aplicações Node.js frequentemente precisam salvar configurações, ler arquivos JSON, processar uploads, gerar relatórios, criar diretórios e mover documentos. O módulo File System no Node.js, disponível como node:fs, oferece as APIs nativas para trabalhar com arquivos e pastas em diferentes sistemas operacionais.
Embora operações de disco pareçam simples, há detalhes importantes: caminhos podem escapar do diretório permitido, arquivos podem mudar entre uma verificação e o uso, gravações podem ficar incompletas e chamadas síncronas podem bloquear o event loop. Uma implementação confiável precisa escolher a API correta, limitar entradas e tratar concorrência.
Neste guia, você aprenderá a usar fs/promises, ler e gravar arquivos, criar diretórios, consultar metadados, copiar, mover e excluir conteúdo, usar streams, observar mudanças e aplicar práticas seguras em produção.
Conhecendo o módulo fs
O Node.js oferece três estilos principais:
- APIs baseadas em callbacks no módulo
node:fs; - APIs baseadas em Promises em
node:fs/promises; - versões síncronas, identificadas pelo sufixo
Sync.
Para código assíncrono moderno, fs/promises costuma ser a escolha mais legível:
const fs = require('node:fs/promises');
async function readConfig() {
const content = await fs.readFile('./config.json', 'utf8');
return JSON.parse(content);
}A documentação oficial do módulo fs apresenta todos os métodos e opções. Para revisar os fundamentos, consulte o que é Node.js e o que é JavaScript.
Lendo um arquivo de texto
const fs = require('node:fs/promises');
async function loadArticle() {
try {
return await fs.readFile('./article.txt', 'utf8');
} catch (error) {
if (error.code === 'ENOENT') {
return '';
}
throw error;
}
}Sem a codificação, readFile() retorna um Buffer. Isso é adequado para imagens, arquivos compactados e outros dados binários. O artigo sobre Buffer no Node.js explica bytes, codificações e limites.
Quando evitar readFile()
readFile() carrega todo o conteúdo na memória. Para arquivos grandes, muitos acessos simultâneos ou downloads, prefira uma stream:
const fs = require('node:fs');
const { pipeline } = require('node:stream/promises');
async function copyLargeFile(source, destination) {
await pipeline(
fs.createReadStream(source),
fs.createWriteStream(destination)
);
}Streams mantêm o consumo de memória mais previsível e controlam backpressure. Veja o guia completo de Streams no Node.js.
Gravando arquivos
const fs = require('node:fs/promises');
await fs.writeFile(
'./output.json',
JSON.stringify({ status: 'ok' }, null, 2),
'utf8'
);Por padrão, o arquivo existente é substituído. A opção flag muda o comportamento:
await fs.writeFile('./log.txt', 'nova linha\n', {
encoding: 'utf8',
flag: 'a'
});Para logs de alto volume, não faça uma chamada de gravação independente por mensagem. Use um logger preparado para buffering, rotação e concorrência.
Gravação atômica
Se outro processo não deve enxergar um arquivo parcialmente escrito, grave primeiro em um nome temporário e depois renomeie:
const fs = require('node:fs/promises');
async function writeJsonAtomic(file, data) {
const temporary = `${file}.${process.pid}.tmp`;
await fs.writeFile(
temporary,
JSON.stringify(data),
'utf8'
);
await fs.rename(temporary, file);
}A renomeação costuma ser atômica quando origem e destino estão no mesmo sistema de arquivos. Trate falhas e remova temporários abandonados durante a inicialização.
Criando diretórios
await fs.mkdir('./storage/reports', {
recursive: true
});Com recursive: true, diretórios intermediários são criados e a chamada não falha apenas porque a pasta final já existe.
Listando arquivos
const entries = await fs.readdir('./uploads', {
withFileTypes: true
});
for (const entry of entries) {
if (entry.isFile()) {
console.log('Arquivo:', entry.name);
}
if (entry.isDirectory()) {
console.log('Diretório:', entry.name);
}
}withFileTypes evita uma chamada de stat() apenas para distinguir arquivo e pasta em muitos casos.
Obtendo metadados
const stats = await fs.stat('./report.pdf');
console.log({
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
isFile: stats.isFile()
});Use lstat() quando precisar inspecionar o link simbólico em vez do destino. Datas e disponibilidade de alguns campos variam conforme o sistema de arquivos.
Verificar antes de usar pode criar corrida
Evite o padrão “verificar se existe e depois abrir”. Entre as duas chamadas, outro processo pode remover ou substituir o arquivo. Tente a operação diretamente e trate o erro:
try {
const content = await fs.readFile(file, 'utf8');
return content;
} catch (error) {
if (error.code === 'ENOENT') return null;
throw error;
}Esse princípio reduz problemas de tempo de verificação versus tempo de uso.
Copiando arquivos e diretórios
await fs.copyFile('./source.txt', './backup.txt');Para árvores de diretórios, versões atuais oferecem fs.cp():
await fs.cp('./public', './backup/public', {
recursive: true,
force: false
});Defina a política para arquivos existentes e links simbólicos de acordo com a finalidade do backup.
Movendo e renomeando
await fs.rename('./draft.txt', './published.txt');Quando os caminhos ficam em sistemas de arquivos diferentes, rename() pode falhar com EXDEV. Nesse caso, copie o conteúdo, confirme a conclusão e só depois remova a origem.
Removendo arquivos
try {
await fs.unlink('./temporary.txt');
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}Para diretórios:
await fs.rm('./temporary-directory', {
recursive: true,
force: true
});recursive: true pode apagar uma árvore inteira. Nunca use um caminho recebido diretamente do cliente.
Construindo caminhos portáveis
Use o módulo node:path em vez de concatenar barras:
const path = require('node:path');
const file = path.join(
process.cwd(),
'storage',
'reports',
'monthly.pdf'
);A documentação oficial de path explica normalização, extensões e caminhos absolutos.
Evitando path traversal
Um usuário pode enviar valores como ../../secret.env. Resolva o caminho e confirme que ele continua dentro da pasta permitida:
const path = require('node:path');
function resolveInside(baseDirectory, requestedName) {
const base = path.resolve(baseDirectory);
const target = path.resolve(base, requestedName);
const prefix = `${base}${path.sep}`;
if (target !== base && !target.startsWith(prefix)) {
throw new Error('Caminho fora da área permitida');
}
return target;
}Também limite os nomes aceitos e gere identificadores internos para uploads, em vez de confiar no nome original.
Permissões
await fs.chmod('./private-key.pem', 0o600);Permissões funcionam de maneira diferente no Windows. Além disso, o valor efetivo pode ser influenciado por umask. Não dependa apenas da permissão do arquivo para proteger segredos; use controles de infraestrutura e criptografia.
FileHandle para várias operações
const handle = await fs.open('./data.bin', 'r');
try {
const buffer = Buffer.alloc(128);
const { bytesRead } = await handle.read(
buffer,
0,
buffer.length,
0
);
console.log(buffer.subarray(0, bytesRead));
} finally {
await handle.close();
}O bloco finally garante o fechamento. Vazamento de descritores pode impedir novas aberturas e degradar o servidor.
Monitorando mudanças
const fsSync = require('node:fs');
const watcher = fsSync.watch('./config', (eventType, filename) => {
console.log({ eventType, filename });
});
process.on('SIGTERM', () => watcher.close());fs.watch() depende dos recursos do sistema operacional. Eventos podem ser agrupados, duplicados ou não trazer um nome. Para tarefas críticas, confirme o estado real do diretório em vez de assumir que cada evento representa exatamente uma mudança.
APIs síncronas
Métodos como readFileSync() bloqueiam o event loop até a operação terminar. Eles podem ser aceitáveis na inicialização, em scripts curtos ou ferramentas de linha de comando, mas devem ser evitados durante requisições de um servidor.
Para entender tarefas externas e scripts, consulte Child Process no Node.js.
Concorrência e bloqueios
Duas gravações simultâneas podem sobrescrever uma à outra. O sistema de arquivos não transforma automaticamente uma sequência de leitura, alteração e gravação em transação. Use uma fila por recurso, arquivos temporários, controle de versão ou um banco de dados quando a consistência for crítica.
Arquivos não são sempre a melhor base para estado compartilhado entre múltiplas instâncias. Em aplicações escaladas horizontalmente, cada contêiner pode possuir disco local independente.
Cancelamento
Algumas operações aceitam AbortSignal. Para streams e tarefas longas, propague um sinal e remova artefatos parciais após o cancelamento. O guia de AbortController no Node.js apresenta timeouts e sinais compostos.
Observabilidade
Registre operação, caminho lógico, duração, tamanho e código do erro. Evite colocar conteúdo ou caminhos contendo dados pessoais nos logs. Monitore descritores abertos, espaço em disco, latência e quantidade de falhas como ENOSPC, EACCES e EMFILE.
Como testar
Crie um diretório temporário exclusivo por teste:
const os = require('node:os');
const path = require('node:path');
const directory = await fs.mkdtemp(
path.join(os.tmpdir(), 'app-test-')
);
try {
// executar teste
} finally {
await fs.rm(directory, {
recursive: true,
force: true
});
}Teste arquivos ausentes, permissões negadas, nomes maliciosos, conteúdo grande, gravação interrompida e operações simultâneas.
Erros comuns
- Usar métodos síncronos em rotas: o event loop fica bloqueado.
- Carregar arquivos enormes: a memória do processo cresce demais.
- Concatenar caminhos do usuário: permite path traversal.
- Verificar existência antes de usar: cria condição de corrida.
- Não fechar FileHandle: descritores ficam vazando.
- Gravar diretamente no destino: consumidores podem enxergar conteúdo parcial.
- Confiar totalmente em fs.watch: eventos não são uniformes entre plataformas.
- Usar disco local como estado distribuído: instâncias divergem.
Boas práticas para produção
- Prefira
fs/promisesem código assíncrono. - Use streams para conteúdo grande.
- Resolva e valide caminhos dentro de uma pasta base.
- Grave em temporário e renomeie para resultados importantes.
- Feche handles em
finally. - Defina limites de tamanho, quantidade e concorrência.
- Trate códigos de erro específicos.
- Monitore espaço em disco e descritores.
- Teste em todos os sistemas operacionais suportados.
- Use armazenamento compartilhado quando houver várias instâncias.
Conclusão
O File System no Node.js fornece recursos completos para ler, escrever, copiar, mover, observar e excluir arquivos. Com fs/promises, streams e FileHandles, é possível criar fluxos eficientes e fáceis de integrar a aplicações assíncronas.
O cuidado principal está nos limites e no caminho dos dados. Evite bloqueio do event loop, valide destinos, trate concorrência e não exponha arquivos parciais. Com essas práticas, o sistema de arquivos pode ser usado com segurança em uploads, relatórios, configurações e tarefas de processamento.




