Node.jsを用いたサーバーサイド開発において、ローカル環境やサーバー上のディレクトリ構造を読み取り、ファイル一覧を取得する操作は非常に頻繁に発生します。

ログファイルの解析、アップロードされた画像の管理、あるいは静的サイトジェネレーターのようなツール開発など、その用途は多岐にわたります。

2026年現在のNode.js環境では、標準モジュールであるfs(File System)モジュールが洗練されており、以前よりも簡潔かつ効率的にファイル操作を行うことが可能になっています。

本記事では、基本的なfs.readdirの使い方から、最新のPromise APIを活用した非同期処理、そして大規模なディレクトリ構造を走査するための再帰的なテクニックまでを詳しく解説します。

Node.jsにおけるファイルシステム操作の基礎

Node.jsでファイルやフォルダを操作するためには、標準で用意されているnode:fsモジュールを使用します。

以前はrequire('fs')と記述するのが一般的でしたが、現在の開発シーンではモジュールであることを明示するnode:プリフィックスを付けたインポート形式が推奨されています。

ファイル一覧の取得には、主に同期的に処理を行うメソッドと、非同期で処理を行うメソッドの2種類が存在します。

同期的なメソッドはコードがシンプルになりますが、実行中にメインスレッドをブロックしてしまうため、高負荷なサーバーアプリケーションでは注意が必要です。

一方で非同期メソッドは、JavaScriptのイベントループを効率的に活用できるため、スケーラビリティが求められる環境では非同期処理の採用が定石となっています。

fs/promisesモジュールの活用

Node.js v10以降、PromiseをベースとしたAPIが導入され、現在ではこのfs/promisesを利用するのが最もモダンな手法です。

従来のコールバック関数を利用する方法に比べ、async/await構文を用いることでコードの可読性が飛躍的に向上します。

エラーハンドリングもtry...catch構文で直感的に記述できるため、開発時のミスを減らす効果も期待できます。

fs.readdirによる基本的な一覧取得

フォルダ内のファイル一覧を取得する最も基本的なメソッドがreaddirです。

このメソッドは指定したパスに含まれるファイル名およびディレクトリ名の配列を返します。

非同期(Promise)での実装例

まずは、最も標準的となるfs/promisesを使用した実装方法を見ていきましょう。

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

async function listFiles(directoryPath) {
  try {
    // readdirでディレクトリ内のファイル名一覧を取得
    const files = await readdir(directoryPath);
    
    // 取得した配列をループで出力
    files.forEach(file => {
      console.log(file);
    });
  } catch (err) {
    // フォルダが存在しない場合や権限がない場合のエラー処理
    console.error('エラーが発生しました:', err);
  }
}

// 実行例
listFiles('./test-folder');
実行結果
index.js
package.json
src
utils

このように、指定したディレクトリの直下にある要素が文字列の配列として取得できます。

ただし、デフォルトの設定では「ファイル」なのか「ディレクトリ」なのかを判別できない点に注意が必要です。

withFileTypesオプションによる判別

ファイルとディレクトリを区別したい場合、readdirの第2引数に{ withFileTypes: true }を指定するのが最も効率的です。

これにより、戻り値が文字列の配列からDirentオブジェクトの配列へと変化します。

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

async function listItemsWithDetails(directoryPath) {
  try {
    const dirents = await readdir(directoryPath, { withFileTypes: true });

    for (const dirent of dirents) {
      if (dirent.isDirectory()) {
        console.log(`[Directory] ${dirent.name}`);
      } else if (dirent.isFile()) {
        console.log(`[File] ${dirent.name}`);
      }
    }
  } catch (err) {
    console.error('読み込み失敗:', err);
  }
}

listItemsWithDetails('./');
実行結果
[File] .env
[Directory] node_modules
[File] package.json
[File] server.js

dirent.isDirectory()dirent.isFile()といったメソッドを使用することで、追加のstat(状態取得)命令を発行することなく種類を判別できます。

これはファイル数が多いディレクトリにおいて、パフォーマンスを維持するための重要なテクニックです。

再帰的なファイル取得の実装

実際の開発では、特定のフォルダだけでなく、その中にあるサブディレクトリも含めたすべてのファイルをリストアップしたい場面が多いでしょう。

Node.js v18.17.0およびv20.0.0以降、readdirにはネイティブで再帰的な取得を行うオプションが追加されました。

recursiveオプションを使用する方法

自分で複雑な再帰関数を書かなくても、オプション一つですべての階層を走査できるようになりました。

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

async function getAllFilesRecursively(directoryPath) {
  try {
    // recursive: true を指定することでサブフォルダ内も走査
    const files = await readdir(directoryPath, { recursive: true });
    
    console.log(files);
  } catch (err) {
    console.error(err);
  }
}

getAllFilesRecursively('./src');
実行結果
[
  'app.js',
  'controllers/userController.js',
  'models/index.js',
  'models/user.js',
  'views/main.html'
]

このオプションを使用すると、パス区切り文字を含んだ相対的なファイルパスが取得できます。

自前で再帰ロジックを組む際に発生しがちな「無限ループ」や「スタックオーバーフロー」のリスクを回避できるため、特別な理由がない限りはrecursiveオプションの使用を推奨します。

大規模ディレクトリ向けの最適化:fs.opendir

数万個、数十万個といった膨大なファイルが含まれるディレクトリを走査する場合、readdirではメモリ消費が問題になることがあります。

readdirは一度にすべてのファイル名をメモリ上の配列に格納してから返すためです。

このようなケースでは、「イテレーター」を利用して一つずつファイルを読み込むfs.opendirが適しています。

opendirによるストリーム処理

JavaScript
import { opendir } from 'node:fs/promises';

async function scanHugeDirectory(path) {
  const dir = await opendir(path);
  
  // 非同期イテレーターを使用して1つずつ処理
  for await (const dirent of dir) {
    console.log('処理中:', dirent.name);
    // ここで重い処理を行っても、メモリに全てを溜め込まない
  }
}

scanHugeDirectory('./huge-assets-folder');

for await...of構文と組み合わせることで、メモリ使用量を最小限に抑えながら巨大なディレクトリを安全にスキャンできます。

これは、リソースが限られたコンテナ環境やラムダ関数などのサーバーレス環境において非常に有効な最適化手法です。

特定のファイル形式だけを抽出する方法

ファイル一覧を取得した後、特定の拡張子(例:.jpgや.pdf)を持つファイルだけに絞り込みたい場合があります。

これにはJavaScript標準の配列メソッドであるfilterを組み合わせるのが一般的です。

拡張子フィルタリングの例

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

async function listImages(directoryPath) {
  const files = await readdir(directoryPath);
  
  // path.extnameを使用して拡張子をチェック
  const imageFiles = files.filter(file => {
    const ext = path.extname(file).toLowerCase();
    return ['.png', '.jpg', '.jpeg', '.gif'].includes(ext);
  });

  console.log('画像ファイル一覧:', imageFiles);
}

OSによって拡張子が大文字(JPG)だったり小文字(jpg)だったりすることがあるため、toLowerCase()で正規化してから比較するのがベストプラクティスです。

また、複雑なパターン(特定のプレフィックスを持つファイルなど)を抽出したい場合は、globライブラリの使用も検討してください。

パフォーマンスと安全性のための比較表

ここまで紹介した手法の使い分けを、以下の表にまとめました。

メソッド主な特徴推奨される利用シーン
fs.readdirSync同期実行。コードが簡潔。起動時の設定読み込みなど、速度を優先しない小規模なスクリプト。
fs.promises.readdir非同期実行。Promiseベース。一般的なWebアプリケーションやAPIでのファイル一覧取得。
recursive: true深い階層まで自動スキャン。プロジェクト全体のファイル構造を把握する場合。
fs.opendirイテレーターによる逐次処理。数千を超える大量のファイルを扱う高負荷なバッチ処理。

よくある落とし穴と注意点

ファイルシステム操作には、特有の注意点がいくつか存在します。

まず第一に、権限(パーミッション)エラーです。

システム上の保護されたディレクトリを読み込もうとすると、Node.jsはエラーをスローしてプロセスを停止させる可能性があります。

必ずtry...catchで囲み、個別のエラーコード(例:EACCES)をハンドリングできるように設計しましょう。

次に、隠しファイル(.gitignoreや.ds_storeなど)の扱いです。

readdirはデフォルトでドットから始まる隠しファイルもすべて取得します。

ユーザーに見せる一覧を作成する場合は、file.startsWith('.')を使って除外する処理を入れるのが一般的です。

最後に、パスの結合には必ずnode:pathモジュールのpath.joinを使用してください。

WindowsとLinuxではパスの区切り文字(\/)が異なるため、文字列連結(+ "/" +)でパスを作るとバグの原因になります。

まとめ

Node.jsでフォルダ内のファイル一覧を取得する方法は、単一の解決策ではなく、用途に応じた最適な選択肢が用意されています。

小規模なツールであればfs.promises.readdirで十分ですが、サブフォルダを含む場合はrecursive: trueオプションが非常に便利です。

また、パフォーマンスを極限まで追求する必要がある大規模システムでは、fs.opendirによるストリーム処理が威力を発揮します。

それぞれのメソッドの特性を理解し、適切に使い分けることで、堅牢で効率的なファイル操作プログラムを構築できるでしょう。

今回紹介したコードをベースに、プロジェクトの要件に合わせたファイル管理機能を実装してみてください。