Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

File System no Node.js: Guia Prático

Atualizado em: 31 de julho de 2026

Discos de armazenamento ilustrando File System no Node.js

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/promises em 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.

Os 10 Melhores Cursos de Programação de 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita