Node.jsにおけるfs(File System)モジュールは、サーバーサイドでローカルファイルシステムを操作するための最も基本的かつ重要な機能の一つです。
ファイルの作成、読み込み、更新、削除といった操作から、ディレクトリの管理やファイル属性の取得まで、多岐にわたる処理を実行できます。
Node.jsがサーバーサイドランタイムとして広く普及した理由の一つに、この強力なファイル操作機能が標準で備わっている点が挙げられます。
本記事では、Node.jsのfsモジュールの基本的な使い方から、2026年現在の開発現場で主流となっているPromise APIを用いたモダンな記述方法まで詳しく紹介します。
Node.js fsモジュールの概要と役割
fsモジュールはNode.jsのコアモジュールであり、外部ライブラリをインストールすることなく利用可能です。
主な役割は、OS(オペレーティングシステム)のファイルシステムとプログラムを仲介することにあります。
Webサーバーのログ保存、設定ファイルの読み込み、静的ファイルの配信など、バックエンド開発における多くの場面で活躍します。
fsモジュールには、その進化の過程で「同期型」「コールバック型」「Promise型」という3つの主要なAPIスタイルが存在するようになりました。
現在のモダンな開発環境では、コードの可読性とメンテナンス性を維持するためにPromise APIが推奨されています。
fsモジュールにおける3つのAPIスタイル
開発を始める前に、fsモジュールが提供する3つの異なるアプローチを理解しておくことが重要です。
それぞれのスタイルには特性があり、用途に応じて使い分ける必要があります。
| スタイル | メソッドの特徴 | 主な利用シーン |
|---|---|---|
| 同期型 (Synchronous) | 処理が完了するまで次のコードを実行しない | CLIツールの初期化や簡易的なスクリプト |
| コールバック型 (Callback) | 非同期で実行され、完了時に、関数を呼び出す | レガシーなシステムや特定のパフォーマンス要件 |
| Promise型 (fs/promises) | async/awaitを使用して非同期処理を記述する | 現在のアプリケーション開発の標準 |
同期型APIの性質
同期型APIは、関数の末尾に Sync という名前が付くのが特徴です。
このメソッドを実行すると、ファイル操作が完全に終了するまでNode.jsのイベントループがブロックされます。
つまり、その間は他のリクエストを処理することができなくなります。
そのため、高トラフィックなWebサーバー内での使用は避け、サーバー起動時の設定読み込みなどに限定するのが一般的です。
const fs = require('fs');
// 同期的にファイルを読み込む例
try {
const data = fs.readFileSync('config.json', 'utf8');
console.log(data);
} catch (err) {
console.error('読み込み失敗:', err);
}
コールバックベースのAPI
コールバックベースのAPIは、Node.jsの初期から存在する非同期処理のスタイルです。
処理の結果を後続の関数(コールバック)に渡すことで、イベントループをブロックせずに動作します。
しかし、複数の非同期処理を組み合わせようとすると「コールバック地獄」と呼ばれるコードの複雑化を招く傾向があります。
const fs = require('fs');
// コールバックによる非同期読み込み
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
PromiseベースのAPI(推奨)
2026年現在のNode.js開発において、最も推奨されるのが Promise API です。
これは fs/promises モジュールから提供され、async/await 構文を利用できます。
同期処理のような見かけの簡潔さと、非同期処理の効率性を両立できる点が最大のメリットです。
const fs = require('fs/promises');
async function readFileExample() {
try {
const data = await fs.readFile('example.txt', 'utf8');
console.log(data);
} catch (err) {
console.error('エラー発生:', err.message);
}
}
readFileExample();
ファイルの読み込みと書き込みの基本操作
ここからは、実務で頻繁に利用する具体的なファイル操作について詳しく見ていきましょう。
基本的な読み書きをマスターすることで、データの永続化や設定管理が容易になります。
テキストファイルの読み込み
ファイルを読み込む際は、fs.readFile() を使用します。
第2引数にエンコーディング(通常は ‘utf8’)を指定しない場合、戻り値は Buffer オブジェクトになります。
テキストとして取得したい場合は、必ずエンコーディングを指定するようにしましょう。
const fs = require('fs/promises');
async function readText() {
// エンコーディングを指定してテキストとして取得
const content = await fs.readFile('note.txt', 'utf8');
console.log('ファイルの内容:', content);
}
ファイルへのデータの書き込みと追記
ファイルへの書き込みには fs.writeFile()、既存のファイル末尾にデータを追加するには fs.appendFile() を使用します。
writeFile は対象のファイルが既に存在する場合、内容を上書きしてしまうため注意が必要です。
また、ディレクトリが存在しない場所に書き込もうとするとエラーが発生するため、事前にディレクトリの存在を確認するか、自動作成オプションを検討する必要があります。
const fs = require('fs/promises');
async function writeExample() {
const data = 'Node.jsの世界へようこそ!\n';
try {
// 新規作成または上書き
await fs.writeFile('output.txt', data);
// データの追記
await fs.appendFile('output.txt', '2026年の新機能も活用しましょう。');
console.log('書き込みが完了しました。');
} catch (err) {
console.error('書き込みエラー:', err);
}
}
ディレクトリの操作とメタ情報の取得
ファイル単体だけでなく、ディレクトリ(フォルダ)の構造を管理することもfsモジュールの重要な役割です。
アプリケーションで使用する一時ディレクトリの作成や、特定のフォルダ内のファイル一覧を取得する際に利用します。
ディレクトリの作成と削除
ディレクトリの作成には mkdir、削除には rm または rmdir を使用します。
特に mkdir で入れ子状のディレクトリ(例: a/b/c)を作成したい場合は、recursive: true オプションを付与するのが一般的です。
const fs = require('fs/promises');
async function manageDirectory() {
try {
// 再帰的にディレクトリを作成
await fs.mkdir('logs/daily/2026', { recursive: true });
// ディレクトリ内のファイル一覧を取得
const files = await fs.readdir('logs');
console.log('ディレクトリ内のリスト:', files);
// ディレクトリの削除(中身がある場合は recursive: true が必要)
// await fs.rm('logs', { recursive: true, force: true });
} catch (err) {
console.error('ディレクトリ操作エラー:', err);
}
}
ファイルのステータス確認
ファイルが存在するかどうかや、それがディレクトリなのかファイルなのかを判定するには fs.stat() を使用します。
また、ファイルの最終更新日時やファイルサイズなどのメタデータも取得可能です。
const fs = require('fs/promises');
async function checkStatus() {
try {
const stats = await fs.stat('output.txt');
console.log('ファイルサイズ:', stats.size);
console.log('これはディレクトリですか?:', stats.isDirectory());
console.log('最終更新日時:', stats.mtime);
} catch (err) {
if (err.code === 'ENOENT') {
console.log('ファイルが存在しません。');
}
}
}
大容量ファイルを効率的に扱うためのStream API
これまでに紹介した readFile などのメソッドは、ファイルの内容を一度にすべてメモリへ読み込みます。
しかし、数GBを超えるような巨大なファイルを扱う場合、メモリ不足(Out of Memory)を引き起こすリスクがあります。
大容量ファイルの処理には、データを分割して少しずつ転送する「Stream API」を利用するのが鉄則です。
Readable Streamによる読み込み
fs.createReadStream() を使用すると、ファイルをチャンク(断片)ごとに読み込むことができます。
これにより、メモリ消費量を一定に保ちながら大規模なデータを処理できるようになります。
const fs = require('fs');
const readStream = fs.createReadStream('large_video.mp4');
readStream.on('data', (chunk) => {
console.log(`受信したデータサイズ: ${chunk.length} バイト`);
});
readStream.on('end', () => {
console.log('すべてのデータの読み込みが完了しました。');
});
受信したデータサイズ: 65536 バイト
受信したデータサイズ: 65536 バイト
...
すべてのデータの読み込みが完了しました。
Writable StreamとPipe
読み込んだデータを別のファイルへ書き出す際は Writable Stream を使用します。
さらに pipe() メソッドを使用することで、読み込みから書き込みまでの流れを簡潔に記述できます。
const fs = require('fs');
const src = fs.createReadStream('source.txt');
const dest = fs.createWriteStream('destination.txt');
// 読み込みストリームを書き込みストリームに直結
src.pipe(dest);
dest.on('finish', () => {
console.log('ファイルのコピーが完了しました。');
});
ファイルやディレクトリの変更を監視する
開発用ツールやサーバーサイドの監視機能を作成する場合、ファイルの変更を検知してアクションを起こしたいことがあります。
fsモジュールには fs.watch() という強力な監視機能が備わっています。
const fs = require('fs');
// ディレクトリやファイルの変更を監視
const watcher = fs.watch('./config', (eventType, filename) => {
console.log(`イベントの種類: ${eventType}`);
if (filename) {
console.log(`変更されたファイル名: ${filename}`);
}
});
// 5分後に監視を終了する例
setTimeout(() => {
watcher.close();
console.log('監視を停止しました。');
}, 300000);
fs.watch() はOS固有の通知機能を利用するため効率的ですが、プラットフォームによって動作が若干異なる場合がある点に留意してください。
エラーハンドリングのベストプラクティス
ファイル操作は、権限不足、ディスク容量不足、パスの誤りなど、外部要因によるエラーが発生しやすい領域です。
堅牢なアプリケーションを構築するためには、適切なエラー処理が欠かせません。
特にPromise APIを使用する場合は、必ず try...catch ブロックで囲むようにしましょう。
また、エラーオブジェクトに含まれる code プロパティを確認することで、エラーの種類に応じた柔軟な対応が可能になります。
| エラーコード | 意味 | 対処法 |
|---|---|---|
| ENOENT | ファイルまたはディレクトリが存在しない | パスを確認するか、作成処理を事前に行う |
| EACCES | アクセス権限がない | 実行ユーザーの権限設定を確認する |
| EEXIST | 既にファイルが存在している | 上書きするか、別の名前に変更する |
const fs = require('fs/promises');
async function robustFileRead(path) {
try {
const data = await fs.readFile(path, 'utf8');
return data;
} catch (err) {
if (err.code === 'ENOENT') {
console.error('指定されたファイルが見つかりません。パスを確認してください。');
} else if (err.code === 'EACCES') {
console.error('ファイルへのアクセス権限がありません。');
} else {
console.error('予期しないエラーが発生しました:', err.message);
}
}
}
まとめ
Node.jsのfsモジュールは、ファイル操作における万能なツールセットです。
かつてはコールバックベースの複雑な記述が必要でしたが、Promise APIの普及により、現在では非常にスマートにコードを記述できるようになりました。
基本的な読み書きから、Streamを用いた高度なメモリ管理まで、用途に合わせた適切なメソッドを選択することが開発の肝となります。
本記事で紹介した「Promise APIの活用」と「適切なエラーハンドリング」を意識することで、より安全で保守性の高いプログラムを構築できるはずです。
ファイルシステムとの連携は、サーバーサイド開発の醍醐味の一つでもあります。
ぜひ、fsモジュールを使いこなし、Node.jsによる自由度の高い開発を楽しんでください。
