Работа с папками в 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, спускаясь в подпапки.