Node.js ファイルパス地図:`fs`/`path`/`URL`/`import.meta.url` の迷わない対応表
Node.js でファイル操作をする際、fs.readFile() に渡すパスはどう書けばいいのか、import.meta.url をどう変換すればいいのか、悩んだことはありませんか?
CommonJS では __dirname を使えば済んでいたのに、ES Modules では import.meta.url が登場し、さらに URL クラスや path モジュールとの使い分けに頭を抱える方も多いでしょう。本記事では、これらのモジュールや API の関係性を整理し、どの場面でどれを使えばいいのかを「対応表」形式で明確にします。
この記事を読めば、パス操作で迷うことがなくなり、自信を持ってコードを書けるようになりますよ。
早見表:各モジュール・API の役割
まずは全体像を把握しましょう。以下の表で、各モジュールや API の役割と主な使用場面を確認できます。
モジュール・API 概要
| モジュール・API | 役割 | 主な使用場面 | 返り値の形式 |
|---|---|---|---|
import.meta.url | 現在のファイルの場所を URL 形式で取得 | ES Modules でのファイル位置の基準点として | file:// URL |
URL クラス | URL の解析・操作・相対パス解決 | 相対パスから絶対 URL を構築する | URL オブジェクト |
path モジュール | ファイルシステムパスの操作・結合・正規化 | 複数のパス要素を結合、クロスプラットフォーム対応 | 文字列パス |
fs モジュール | 実際のファイル読み書き | ファイル・ディレクトリの操作 | データまたは void |
fileURLToPath() | file:// URL を文字列パスに変換 | URL を path や fs で使える形式に変換 | 文字列パス |
pathToFileURL() | 文字列パスを file:// URL に変換 | パスを URL 形式に変換 | URL オブジェクト |
主要メソッド早見表
| メソッド | 用途 | 入力例 | 出力例 |
|---|---|---|---|
path.join() | 複数のパスを結合 | join('/users', 'name', 'file.txt') | /users/name/file.txt |
path.resolve() | 絶対パスを構築(作業ディレクトリ基準) | resolve('data', 'config.json') | /current/dir/data/config.json |
path.dirname() | パスからディレクトリ部分を取得 | dirname('/users/name/file.txt') | /users/name |
path.basename() | パスからファイル名を取得 | basename('/users/name/file.txt') | file.txt |
path.extname() | パスから拡張子を取得 | extname('file.txt') | .txt |
path.normalize() | パスを正規化(.. や . を解決) | normalize('/path/to/../file.txt') | /path/file.txt |
new URL(rel, base) | 相対パスから絶対 URL を構築 | new URL('./data.json', import.meta.url) | URL オブジェクト |
fileURLToPath(url) | file:// URL を文字列パスに変換 | fileURLToPath(new URL(import.meta.url)) | /Users/name/project/src/index.js |
pathToFileURL(path) | 文字列パスを file:// URL に変換 | pathToFileURL('/users/name/file.txt') | file:///users/name/file.txt |
よくある操作パターン
| やりたいこと | 推奨コード |
|---|---|
| 現在のファイルのディレクトリを取得 | dirname(fileURLToPath(import.meta.url)) |
| 同じディレクトリのファイルを読み込む | readFileSync(new URL('./file.json', import.meta.url)) |
| 親ディレクトリのファイルにアクセス | new URL('../data/file.json', import.meta.url) |
| 複数のパス要素を安全に結合 | join(__dirname, 'data', 'users', 'profile.json') |
| 作業ディレクトリからの絶対パスを構築 | resolve('logs', 'app.log') |
| パスが存在するか確認してから読み込む | if (existsSync(path)) { readFileSync(path) } |
背景
CommonJS から ES Modules への移行
Node.js は長らく CommonJS 形式のモジュールシステムを採用してきました。CommonJS では __dirname や __filename といったグローバル変数が自動的に提供され、現在のファイルの場所を簡単に取得できました。
javascript// CommonJS の例
const path = require('path');
const dataPath = path.join(
__dirname,
'data',
'config.json'
);
しかし、ES Modules (ESM) が標準化されると、Node.js もこれをサポートするようになりました。ESM では __dirname や __filename が使えなくなり、代わりに import.meta.url が導入されました。
様々なパス形式の登場
Node.js のファイルパスには、以下のような複数の形式が存在します。
| # | パス形式 | 例 | 用途 |
|---|---|---|---|
| 1 | 絶対パス(POSIX) | /Users/name/project/file.txt | Unix 系 OS でのファイルシステムパス |
| 2 | 絶対パス(Windows) | C:\Users\name\project\file.txt | Windows でのファイルシステムパス |
| 3 | 相対パス | ./data/config.json | 現在のディレクトリからの相対位置 |
| 4 | file:// URL | file:///Users/name/project/file.txt | URL 形式のファイルパス |
これらの形式を適切に変換・操作するために、path モジュール、URL クラス、import.meta.url といった様々なツールが用意されています。
次の図は、これらのパス形式がどのように関連しているかを示したものです。
mermaidflowchart TB
esmFile["ES Modules<br />ファイル"]
importMetaUrl["import.meta.url<br />(file:// URL)"]
urlClass["URL クラス"]
fileURLToPath["fileURLToPath()"]
absPath["絶対パス<br />(文字列)"]
pathModule["path モジュール"]
relativePath["相対パス"]
fsModule["fs モジュール"]
esmFile --|自動提供|--> importMetaUrl
importMetaUrl --> urlClass
importMetaUrl --> fileURLToPath
urlClass --|new URL(相対, base)|--> urlClass
urlClass --|pathname|--> absPath
fileURLToPath --|変換|--> absPath
absPath --> pathModule
pathModule --|join/resolve|--> absPath
absPath --> fsModule
relativePath --> pathModule
pathModule --> fsModule
上記の図から、import.meta.url が起点となり、各モジュールがパスを変換・操作していることが理解できます。
モジュール間の役割分担
各モジュールや API には、明確な役割があります。
import.meta.url:現在の ES Module ファイルの URL をfile://形式で返すURLクラス:URL の解析・操作を行う(相対パスの解決など)pathモジュール:ファイルシステムパスの操作(結合、正規化など)fsモジュール:実際のファイル読み書きを行う
これらを適切に組み合わせることで、クロスプラットフォームで動作する堅牢なファイル操作が可能になります。
課題
__dirname が使えない ES Modules
CommonJS では当たり前だった __dirname が ES Modules では使えません。
javascript// ES Modules で __dirname を使うとエラー
console.log(__dirname);
// ReferenceError: __dirname is not defined
このため、現在のファイルの場所を基準にした相対パスの解決ができず、困ってしまいます。
import.meta.url は file:// URL 形式
ES Modules で提供される import.meta.url は file:// で始まる URL 形式です。
javascript// import.meta.url の出力例
console.log(import.meta.url);
// file:///Users/name/project/src/index.js
しかし、fs モジュールの多くの関数は文字列パスを期待しているため、そのまま渡すとエラーになることがあります。
javascriptimport fs from 'fs';
// これはエラーになる可能性がある
fs.readFileSync(import.meta.url);
// TypeError: The "path" argument must be of type string or an instance of Buffer or URL.
相対パスの解決方法が複数ある
現在のファイルから相対的な位置にあるファイルを読み込む場合、以下のような複数の方法が考えられます。
URLクラスを使う方法path.join()とfileURLToPath()を組み合わせる方法path.resolve()を使う方法
どの方法を選ぶべきか、明確な基準がないと混乱してしまいますね。
クロスプラットフォーム対応の難しさ
Windows と Unix 系 OS ではパスの区切り文字が異なります(\ vs /)。手動で文字列結合すると、プラットフォームごとに動作が異なる可能性があります。
javascript// 悪い例:手動でパスを結合
const badPath = '/Users/name/project' + '/' + 'data.json';
// Windows では動かない可能性がある
これらの課題を解決するには、各 API の特性を理解し、適切に使い分ける必要があります。
解決策
基本原則:パス形式の変換フロー
Node.js でファイルパスを扱う際の基本フローは以下のとおりです。
mermaidflowchart LR
start["import.meta.url<br/>(file:// URL)"]
convert1["fileURLToPath()"]
dirname["ディレクトリパス<br/>(文字列)"]
join["path.join() /<br/>path.resolve()"]
targetPath["目的のパス<br/>(文字列)"]
fs["fs 操作"]
start --> convert1
convert1 --> dirname
dirname --> join
join --> targetPath
targetPath --> fs
style start fill:#e1f5ff
style targetPath fill:#fff4e1
style fs fill:#e8f5e9
この図から、「URL → 文字列パス → 操作 → fs」という流れが基本であることが分かります。
迷わない対応表:状況別の使い分け
以下の対応表を使えば、どの場面でどの API を使うべきかが一目で分かります。
| # | やりたいこと | 使う API | 入力 | 出力 |
|---|---|---|---|---|
| 1 | 現在のファイルのディレクトリを取得 | fileURLToPath() + path.dirname() | import.meta.url | /Users/name/project/src |
| 2 | 現在のファイルから相対パスで別ファイルを指定(URL) | new URL(relative, base) | './data.json', import.meta.url | URL オブジェクト |
| 3 | 現在のファイルから相対パスで別ファイルを指定(文字列) | path.join() | __dirname相当, 'data.json' | /Users/name/project/src/data.json |
| 4 | 絶対パスを構築 | path.resolve() | './data', 'config.json' | /Users/name/project/data/config.json |
| 5 | パスの正規化(.. や . を解決) | path.normalize() | '/path/to/../file.txt' | /path/file.txt |
| 6 | パスの拡張子を取得 | path.extname() | 'file.txt' | .txt |
| 7 | file:// URL を文字列パスに変換 | fileURLToPath() | 'file:///path/to/file' | /path/to/file |
| 8 | 文字列パスを file:// URL に変換 | pathToFileURL() | /path/to/file | URL オブジェクト |
推奨パターン 1:現在のファイルの場所を基準にする
ES Modules で __dirname 相当の値を取得するには、以下のパターンを使います。
javascriptimport { fileURLToPath } from 'url';
import { dirname } from 'path';
// 現在のファイルのパスを取得
const __filename = fileURLToPath(import.meta.url);
// 現在のファイルのディレクトリを取得
const __dirname = dirname(__filename);
このパターンは、ES Modules でのファイル操作の基本となります。
推奨パターン 2:相対パスからファイルを読み込む(URL 利用)
URL クラスを使うと、相対パスの解決が簡潔に書けます。
javascriptimport { readFileSync } from 'fs';
// 現在のファイルと同じディレクトリにある data.json を読み込む
const dataUrl = new URL('./data.json', import.meta.url);
const data = readFileSync(dataUrl, 'utf-8');
fs モジュールの多くの関数は URL オブジェクトを直接受け取れるため、fileURLToPath() での変換が不要です。
推奨パターン 3:複数のパスを結合する(path 利用)
複数のディレクトリやファイル名を組み合わせる場合は、path.join() が便利です。
javascriptimport { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { readFileSync } from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
// 複数のパス要素を結合
const configPath = join(
__dirname,
'..',
'config',
'settings.json'
);
const config = readFileSync(configPath, 'utf-8');
path.join() は OS ごとの区切り文字を自動的に処理してくれるため、クロスプラットフォーム対応が容易です。
推奨パターン 4:絶対パスを構築する
path.resolve() を使うと、現在の作業ディレクトリを基準とした絶対パスを構築できます。
javascriptimport { resolve } from 'path';
// プロセスの作業ディレクトリからの絶対パスを構築
const logPath = resolve('logs', 'app.log');
console.log(logPath);
// /Users/name/project/logs/app.log (実行場所に依存)
ただし、path.resolve() は実行時の process.cwd() に依存するため、スクリプトの実行場所によって結果が変わる点に注意が必要です。
エラー処理:TypeError: The "path" argument must be...
もし以下のようなエラーが出た場合は、file:// URL を文字列パスに変換していない可能性があります。
javascriptTypeError: The "path" argument must be of type string or an instance of Buffer or URL.
解決方法:
URLオブジェクトをそのまま渡すfileURLToPath()で文字列に変換してから渡す
javascriptimport { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
// 方法1:URL オブジェクトをそのまま渡す(推奨)
const url = new URL('./data.json', import.meta.url);
readFileSync(url, 'utf-8');
// 方法2:文字列に変換してから渡す
const path = fileURLToPath(url);
readFileSync(path, 'utf-8');
具体例
例 1:設定ファイルを読み込む
現在の ES Module ファイルと同じディレクトリにある config.json を読み込む完全な例です。
ディレクトリ構造:
arduinoproject/
├── src/
│ ├── index.js
│ └── config.json
インポート部分:
javascriptimport { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
必要なモジュールをインポートします。fs はファイル読み込み、url は URL → パス変換、path はパス操作に使用します。
現在のディレクトリを取得:
javascript// 現在のファイルのパスを取得
const __filename = fileURLToPath(import.meta.url);
// 現在のファイルのディレクトリを取得
const __dirname = dirname(__filename);
console.log(__dirname);
// /Users/name/project/src
import.meta.url から __dirname 相当の値を作成しています。
設定ファイルを読み込む:
javascript// 同じディレクトリの config.json を読み込む
const configPath = join(__dirname, 'config.json');
const configData = readFileSync(configPath, 'utf-8');
const config = JSON.parse(configData);
console.log(config);
// { "appName": "MyApp", "version": "1.0.0" }
path.join() でパスを結合し、fs.readFileSync() でファイルを読み込んでいます。
例 2:親ディレクトリのファイルにアクセス
現在のファイルから 1 階層上の data ディレクトリにある users.json を読み込みます。
ディレクトリ構造:
cssproject/
├── data/
│ └── users.json
└── src/
└── index.js
URL クラスを使う方法:
javascriptimport { readFileSync } from 'fs';
// 相対パスを URL で解決
const usersUrl = new URL(
'../data/users.json',
import.meta.url
);
const usersData = readFileSync(usersUrl, 'utf-8');
const users = JSON.parse(usersData);
console.log(users);
// [{ "id": 1, "name": "Alice" }, { "id": 2, "name": "Bob" }]
new URL() の第 1 引数に相対パス、第 2 引数に基準 URL を指定すると、自動的に解決してくれます。
path モジュールを使う方法:
javascriptimport { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
// ..で親ディレクトリに移動してから data/users.json を指定
const usersPath = join(
__dirname,
'..',
'data',
'users.json'
);
const usersData = readFileSync(usersPath, 'utf-8');
const users = JSON.parse(usersData);
path.join() を使う場合は、.. で親ディレクトリを明示的に指定します。
例 3:動的にファイル名を構築
ユーザー ID に基づいて、動的にファイルパスを構築する例です。
ディレクトリ構造:
cproject/
└── src/
├── index.js
└── logs/
├── user_1.log
├── user_2.log
└── user_3.log
ファイルパスの動的構築:
javascriptimport { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
まず基本的な準備を行います。
ユーザー ID からログファイルを読み込む関数:
javascript// ユーザーIDからログファイルのパスを生成して読み込む
function getUserLog(userId) {
// ファイル名を動的に構築
const filename = `user_${userId}.log`;
// パスを結合
const logPath = join(__dirname, 'logs', filename);
// ファイルを読み込む
return readFileSync(logPath, 'utf-8');
}
join() を使うことで、ディレクトリ区切り文字を意識せずにパスを構築できます。
使用例:
javascript// ユーザー1のログを取得
const log1 = getUserLog(1);
console.log(log1);
// 2024-01-15: User 1 logged in
// ユーザー2のログを取得
const log2 = getUserLog(2);
console.log(log2);
// 2024-01-16: User 2 updated profile
この方法なら、ユーザー数が増えてもコードを変更する必要がありません。
例 4:パスの存在確認とエラーハンドリング
ファイルが存在しない場合のエラー処理を含む堅牢な実装例です。
必要なモジュールのインポート:
javascriptimport { readFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
安全なファイル読み込み関数:
javascript// ファイルを安全に読み込む関数
function safeReadFile(relativePath) {
// 絶対パスを構築
const absolutePath = join(__dirname, relativePath);
// ファイルの存在確認
if (!existsSync(absolutePath)) {
throw new Error(`File not found: ${absolutePath}`);
}
// ファイルを読み込む
try {
return readFileSync(absolutePath, 'utf-8');
} catch (error) {
throw new Error(
`Failed to read file: ${error.message}`
);
}
}
existsSync() で事前にファイルの存在を確認し、try-catch でエラーを捕捉しています。
エラーハンドリング付きの使用例:
javascript// 使用例
try {
const data = safeReadFile('config.json');
console.log('Config loaded:', data);
} catch (error) {
console.error('Error:', error.message);
// Error: File not found: /Users/name/project/src/config.json
// デフォルト設定を使用するなどのフォールバック処理
console.log('Using default configuration');
}
このようにエラーハンドリングを組み込むことで、実用的なアプリケーションを構築できます。
例 5:複数のファイルをまとめて処理
複数の設定ファイルを読み込んでマージする実例です。
ディレクトリ構造:
arduinoproject/
└── src/
├── index.js
└── config/
├── base.json
├── development.json
└── production.json
複数ファイルの読み込み処理:
javascriptimport { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
// 環境変数から環境を取得(デフォルトは development)
const env = process.env.NODE_ENV || 'development';
設定ファイルのマージ関数:
javascript// 複数の設定ファイルを読み込んでマージ
function loadConfig() {
const configDir = join(__dirname, 'config');
// base.json を読み込む
const basePath = join(configDir, 'base.json');
const baseConfig = JSON.parse(
readFileSync(basePath, 'utf-8')
);
// 環境別の設定ファイルを読み込む
const envPath = join(configDir, `${env}.json`);
const envConfig = JSON.parse(
readFileSync(envPath, 'utf-8')
);
// 設定をマージ(環境別設定が優先)
return { ...baseConfig, ...envConfig };
}
複数のファイルを path.join() で組み立て、スプレッド構文でマージしています。
マージ結果の使用:
javascript// 設定を読み込んで使用
const config = loadConfig();
console.log('Final config:', config);
// {
// "appName": "MyApp", // base.json から
// "port": 3000, // base.json から
// "debug": true, // development.json から(上書き)
// "apiUrl": "http://localhost:8000" // development.json から
// }
この手法は、環境ごとに異なる設定を管理する際に非常に便利ですね。
例 6:Windows と macOS の両方で動作するパス処理
クロスプラットフォーム対応のファイル操作の例です。
プラットフォーム依存のパス情報を確認:
javascriptimport { sep, delimiter } from 'path';
// 現在のOSのパス区切り文字を表示
console.log('Path separator:', sep);
// macOS/Linux: /
// Windows: \
// 環境変数のパス区切り文字を表示
console.log('Path delimiter:', delimiter);
// macOS/Linux: :
// Windows: ;
クロスプラットフォームなパス構築:
javascriptimport { join, normalize } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
// 良い例:path.join() を使う
const goodPath = join(
__dirname,
'data',
'users',
'profile.json'
);
console.log(goodPath);
// macOS: /Users/name/project/src/data/users/profile.json
// Windows: C:\Users\name\project\src\data\users\profile.json
path.join() は OS に応じて自動的に適切な区切り文字を使用します。
パスの正規化:
javascript// 複雑なパスを正規化する
const messyPath = join(
__dirname,
'data',
'..',
'config',
'.',
'settings.json'
);
const cleanPath = normalize(messyPath);
console.log('Messy:', messyPath);
console.log('Clean:', cleanPath);
// Clean: /Users/name/project/src/config/settings.json
normalize() は .. や . を解決し、余分な区切り文字を削除してくれます。
まとめ
本記事では、Node.js でのファイルパス操作における fs、path、URL、import.meta.url の使い分けを整理しました。
重要なポイント:
- ES Modules では
import.meta.urlが起点となり、これをfileURLToPath()で文字列パスに変換するのが基本パターンです - 相対パスの解決には
new URL()が便利で、fsモジュールに直接渡せるため簡潔なコードが書けます - 複数のパス要素を結合する場合は
path.join()を使い、OS 間の互換性を確保しましょう - 絶対パスの構築には
path.resolve()を使いますが、実行ディレクトリに依存する点に注意が必要です - エラーハンドリングとして、ファイル存在確認と try-catch を組み合わせることで堅牢なコードになります
状況別の推奨メソッド:
| # | 状況 | 推奨メソッド |
|---|---|---|
| 1 | 現在のファイルと同じ場所のファイルを読む | new URL('./file.json', import.meta.url) |
| 2 | 親ディレクトリのファイルにアクセス | new URL('../data/file.json', import.meta.url) または path.join(__dirname, '..', 'data', 'file.json') |
| 3 | 複数のパス要素を組み合わせる | path.join() |
| 4 | 現在の作業ディレクトリからの絶対パス | path.resolve() |
| 5 | パスの正規化 | path.normalize() |
| 6 | file:// URL と文字列パスの相互変換 | fileURLToPath() / pathToFileURL() |
これらの対応表とパターンを覚えておけば、Node.js でのファイルパス操作で迷うことはなくなるでしょう。CommonJS から ES Modules への移行も、自信を持って進められますね。
実際のプロジェクトでは、まず __dirname 相当の値を取得するヘルパー関数を用意し、その後は path.join() や new URL() を使ってパスを組み立てるという流れが基本になります。この基本フローを押さえておけば、どんな複雑なファイル操作も対応できるようになりますよ。
関連リンク
articleNode.js ファイルパス地図:`fs`/`path`/`URL`/`import.meta.url` の迷わない対応表
articleNode.js プロジェクト初期化テンプレ:ESM 前提の `package.json` 設計と `exports` ルール
articleNode.js 標準テストランナー完全理解:`node:test` がもたらす新しい DX
articleNode.js で ESM の `ERR_MODULE_NOT_FOUND` を解く:解決策総当たりチェックリスト
articleNode.js 本番メモリ運用:ヒープ/外部メモリ/リーク検知の継続監視
articleNode.js で社内 RPA:Playwright でブラウザ自動化&失敗回復の流儀
articleTauri vs Electron vs Flutter デスクトップ:UX・DX・配布のしやすさ徹底比較
articleRuby と Python を徹底比較:スクリプト・Web・データ処理での得意分野
articleshadcn/ui のバンドルサイズ影響を検証:Tree Shaking・Code Split の実測データ
articleRedis Docker Compose 構築:永続化・監視・TLS まで 1 ファイルで
articleRemix を選ぶ基準:認証・API・CMS 観点での要件適合チェック
articleReact で管理画面を最短構築:テーブル・フィルタ・権限制御の実例
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来