Работа с папками в Node.js: mkdir, readdir, rm

Содержание

О проверке кода. Примеры не запускались — в отличие от материалов с бейджем. Код дан по документации Node.js. Говорим прямо, а не ставим бейдж «проверено».

Работа с каталогами — частая задача, которую раньше решали сторонними пакетами (mkdirp, rimraf). Сегодня всё нужное встроено в node:fs/promises. Разберём.

Создание: recursive решает всё

import { mkdir } from 'node:fs/promises';

// ❌ упадёт, если родителя нет
await mkdir('logs/2026/july');   // ENOENT, если нет logs/2026

// ✅ создаст всю цепочку и не тронет существующее
await mkdir('logs/2026/july', { recursive: true });

Флаг recursive: true — почти всегда правильный выбор. Он делает две вещи: создаёт всю недостающую цепочку каталогов и не падает, если папка уже существует. Без него mkdir требует, чтобы родитель существовал, и ругается на повторное создание.

Именно recursive заменил пакет mkdirp, который годами ставили ради этой одной возможности.

Чтение: withFileTypes экономит stat

import { readdir } from 'node:fs/promises';

// просто имена — по ним не понять, файл или папка
await readdir('src');
// ['index.js', 'utils', 'config.json']

// с типами — сразу видно
const entries = await readdir('src', { withFileTypes: true });
for (const e of entries) {
  console.log(e.name, e.isDirectory() ? 'папка' : 'файл');
}

По умолчанию readdir возвращает только имена — строки, по которым не отличить файл от папки. Наивное решение — вызвать stat на каждый элемент, но это лишний системный вызов на каждый файл.

withFileTypes: true возвращает объекты Dirent с методами isFile(), isDirectory(), isSymbolicLink(). Тип приходит вместе со списком, без дополнительных запросов — на каталоге из тысяч файлов разница ощутима.

Удаление: rm вместо rmdir

import { rm, unlink } from 'node:fs/promises';

await unlink('file.txt');                          // один файл
await rm('temp', { recursive: true, force: true }); // папка с содержимым
Удаление: что чем

Один файл

unlink(path)

  • Удаляет файл
  • На папке — ошибка
  • Несуществующий → ENOENT

Папка с содержимым

rm(dir, { recursive: true, force: true })

  • recursive — удаляет вложенное
  • force — не падает, если нет
  • Замена rmdir и пакета rimraf
  • ⚠️ удаляет молча и безвозвратно

Пара recursive + force — современная замена и устаревшего rmdir (он не удалял непустые папки), и внешнего rimraf. Осторожно: rm с этими флагами удаляет молча и без корзины. rm('/', { recursive: true, force: true }) сделает ровно то, чего вы боитесь, — проверяйте путь.

Обход дерева каталогов

Рекурсивный обход раньше писали руками. С Node 18.17 — одна опция:

// все вложенные пути одним вызовом
const all = await readdir('project', { recursive: true });
// ['src', 'src/index.js', 'src/utils', 'src/utils/log.js', ...]

Когда нужен контроль (фильтр по ходу, ранний выход) — пишут свою рекурсию:

import { readdir } from 'node:fs/promises';
import { join } from 'node:path';

async function* walk(dir) {
  for (const e of await readdir(dir, { withFileTypes: true })) {
    const path = join(dir, e.name);
    if (e.isDirectory()) yield* walk(path);   // спускаемся в подпапку
    else yield path;                           // отдаём файл
  }
}

for await (const file of walk('src')) {
  console.log(file);
}

Здесь встречаются сразу три идеи с этого сайта: рекурсия, генераторы (yield) и path.join для кроссплатформенных путей. for await по такому генератору — читаемый способ обойти дерево лениво, не собирая весь список в память сразу.

Копирование и перемещение

import { cp, rename, copyFile } from 'node:fs/promises';

await copyFile('a.txt', 'b.txt');                  // один файл
await cp('src', 'backup', { recursive: true });    // папку целиком
await rename('old-name', 'new-name');              // переименовать/переместить

cp с recursive копирует каталог со всем содержимым — тоже относительно новое (Node 16.7) и тоже убирает нужду во внешних пакетах. rename работает и как переименование, и как перемещение — но только в пределах одной файловой системы; между дисками нужно копировать и удалять.

Ошибки — по коду

Как и везде в fs, различайте ошибки по err.code:

Код Значение
ENOENT пути нет
EEXIST уже существует (без recursive)
ENOTEMPTY папка не пуста (старый rmdir)
EACCES нет прав
ENOTDIR ожидалась папка, а это файл

Смежные темы: модуль fs целиком; модуль path — сборка путей; потоки — чтение больших файлов из каталога. Полный список — в справочнике по Node.js.

Частые вопросы

Как создать вложенные папки одной командой?
mkdir(dir, { recursive: true }). Флаг recursive создаёт всю цепочку недостающих каталогов и не падает, если папка уже есть. Без него mkdir на несуществующем родителе выдаст ошибку.
Как узнать, файл это или папка?
readdir(dir, { withFileTypes: true }) возвращает объекты Dirent с методами isFile() и isDirectory(). Это экономит вызов stat на каждый элемент — на больших каталогах заметно быстрее.
Как удалить папку с содержимым?
rm(dir, { recursive: true, force: true }). recursive удаляет вложенное, force не падает, если папки нет. Это современная замена rmdir и пакета rimraf.
Как обойти дерево каталогов рекурсивно?
readdir(dir, { recursive: true }) вернёт все вложенные пути одним вызовом (с Node 18.17). Либо пишут свою рекурсию через readdir с withFileTypes, спускаясь в подпапки.
Нужен ли пакет rimraf или mkdirp в 2026?
Нет. Всё, ради чего их брали, встроено: mkdir с recursive заменил mkdirp, rm с recursive+force заменил rimraf. Это лишние зависимости в новом коде.