Node.jsでファイルのパスを組み立てるとき、文字列を+でつなぐと、区切り文字(/や\)の付け忘れや二重付け、WindowsとLinuxの違いでバグが起きます。これを安全に処理してくれるのが標準のpathモジュールです。
使い方はシンプルですが、最大の混乱はpath.joinとpath.resolveの違いです。どちらもパスをつなぎますが、joinは単純な連結、resolveは絶対パスを作ります。この違いを知らないと、意図しない場所を指してしまいます。この記事では、実機のNode.jsで確認しながら、pathの使い方を整理します。
- パスの連結は
path.join(...)。区切り文字を自動で正しく入れてくれます。 path.joinは相対的な連結、path.resolveは絶対パスを作ります。用途で使い分けます。- ファイル名は
path.basename、フォルダはpath.dirname、拡張子はpath.extnameです。 - まとめて分解するなら
path.parse。{ root, dir, base, ext, name }が得られます。 - ファイルの場所を基準にするなら、
path.join(__dirname, "...")を使います。 - 区切り文字はOSで変わります(Windowsは
\、Linuxは/)。常に/で扱いたいならpath.posixです。
ESモジュールで__dirnameを使う方法はrequireとimportの違い、パスを使ったファイル操作はfsでファイルを読み書きする方法、依存管理はnpmとpackage.jsonの基礎もあわせて参考になります。
なぜpathモジュールを使うのか
パスを文字列の+でつなぐと、思わぬバグが起きます。区切り文字が二重になったり、OSによって/と\が混ざったりするためです。
// NG: 文字列結合は区切りのミスやOS差が起きやすい
const dir = "logs/";
const file = "/app.log";
const p1 = dir + file; // "logs//app.log"(区切りが二重)
// OK: path.join なら区切りを正しく処理する
const path = require("path");
const p2 = path.join("logs", "app.log"); // logs\app.log(環境に合った区切り)
path.joinは、余分な区切りを取り除き、その環境に合った区切り文字でつないでくれます。手で文字列を組み立てるより、はるかに安全です。
path.joinでパスをつなぐ
path.joinは、渡したパスの断片を区切り文字でつなぎます。..(上の階層)なども正しく整理してくれます。
const path = require("path");
// 複数の断片をつなぐ(Windowsでの実行結果)
path.join("foo", "bar", "baz.txt"); // foo\bar\baz.txt
// .. は整理される(bar を1つ上がる)
path.join("foo", "bar", "..", "baz.txt"); // foo\baz.txt
// 余分な区切りもまとめられる
path.join("logs/", "/app.log"); // logs\app.log
実機(Windows)では、path.join("foo", "bar", "baz.txt")がfoo\bar\baz.txtになりました。Linux環境なら区切りは/になり、foo/bar/baz.txtです。区切り文字をコードに直接書かず、path.joinに任せることで、どのOSでも正しく動きます。
【最重要】joinとresolveの違い(相対と絶対)
path.joinとよく似たpath.resolveがありますが、性質が違います。joinは断片を連結するだけ、resolveは絶対パスを組み立てるのが目的です。
const path = require("path");
// join: そのまま連結(相対パスのまま)
path.join("a", "b"); // a\b
// resolve: 現在の作業フォルダを起点に絶対パスにする
path.resolve("a", "b"); // C:\現在のフォルダ\a\b
// resolve は、途中に絶対パスがあるとそれ以前を無視する
path.resolve("foo", "C:\\abs", "bar"); // C:\abs\bar
// 「アプリの起点からの絶対パス」がほしいときは resolve
const root = path.resolve("config", "db.json");
実機で確認したところ、path.join("a", "b")はa\b(相対)、path.resolve("a", "b")はC:\現在のフォルダ\a\b(絶対)になりました。resolveは、指定がすべて相対なら現在の作業フォルダ(カレントディレクトリ)を起点にします。さらに、途中に絶対パスが現れると、それより前は無視されます。「今いるフォルダに依存しない確実な絶対パスがほしい」なら、次に説明する__dirnameと組み合わせます。
ファイル名・フォルダ・拡張子を取り出す
パスから一部分を取り出すには、basename(ファイル名)、dirname(フォルダ)、extname(拡張子)を使います。
const path = require("path");
const p = "/dir/sub/report.txt";
path.basename(p); // report.txt(ファイル名)
path.basename(p, ".txt"); // report(拡張子を除く)
path.dirname(p); // /dir/sub(フォルダ部分)
path.extname(p); // .txt(拡張子)
path.extname("README"); // ""(拡張子なしは空文字)
実機でも、path.basename("/dir/sub/report.txt")はreport.txt、第2引数に".txt"を渡すと拡張子を除いたreportになりました。拡張子で処理を分けたいときはpath.extnameが便利です。拡張子の無いファイルでは空文字が返ります。
パスを分解する(path.parse)
パスの各部分をまとめて取り出すならpath.parseが便利です。オブジェクトとして、ルート・フォルダ・ファイル名・拡張子・拡張子なしの名前が得られます。
const path = require("path");
const info = path.parse("C:\\dir\\report.txt");
console.log(info);
// {
// root: "C:\\",
// dir: "C:\\dir",
// base: "report.txt",
// ext: ".txt",
// name: "report"
// }
console.log(info.name); // report
console.log(info.ext); // .txt
実機でも、path.parseはroot・dir・base・ext・nameを持つオブジェクトを返しました。name(拡張子なしのファイル名)とextを組み合わせれば、「拡張子を変えた新しいファイル名」なども簡単に作れます。逆に部品からパスを組み立てるpath.formatもあります。
__dirnameを基準に絶対パスを作る
スクリプトと同じフォルダにある設定ファイルを読む、といった処理では、そのファイルの場所を基準にパスを作ります。CommonJSでは__dirname(このファイルがあるフォルダ)が使えます。
const path = require("path");
const fs = require("fs");
// このスクリプトと同じフォルダの config.json を読む
const configPath = path.join(__dirname, "config.json");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
// 1つ上のフォルダの data を指す
const dataDir = path.join(__dirname, "..", "data");
注意点として、ESモジュール(.mjsや"type": "module")では__dirnameが使えません。代わりにimport.meta.urlから作ります。import { fileURLToPath } from "node:url"を使い、path.dirname(fileURLToPath(import.meta.url))とすると同じものが得られます。詳しくはrequireとimportの違いで解説しています。__dirnameを基準にすれば、どこから実行しても同じファイルを正しく指せます。
OSの区切り文字(sep / posix)
区切り文字はOSによって違います。Windowsは\、Linux・macOSは/です。現在の区切り文字はpath.sepで取得できます。常に/で扱いたい場合はpath.posixを使います。
const path = require("path");
path.sep; // Windows は \ 、Linux は /(区切り文字そのもの)
// 常に / で連結したい(URL組み立てなど)
path.posix.join("foo", "bar", "baz.txt"); // foo/bar/baz.txt
// 常に \ で扱いたい(Windows形式を強制)
path.win32.join("foo", "bar"); // foo\bar
// 絶対パスかどうかの判定
path.isAbsolute("C:\\x"); // true
path.isAbsolute("foo"); // false
実機(Windows)では、path.sepが\、path.posix.joinは常に/区切りのfoo/bar/baz.txtを返しました。URLのパス部分を組み立てるときは、OSに関係なく/を使うpath.posixが便利です。通常のファイル操作では、OSに合わせてくれるpath.joinをそのまま使えば問題ありません。
よくある失敗
パスを文字列の+でつないで区切りを間違える
+は区切りの二重付けや付け忘れが起きます。path.joinを使えば、区切りを正しく処理してくれます。
joinとresolveを取り違える
joinは相対的な連結、resolveは絶対パスの組み立てです。確実な絶対パスがほしいならresolveか__dirnameとの組み合わせを使います。
区切り文字を\や/で直接書く
OSによって区切りが違います。直接書かず、path.joinに任せます。URL用ならpath.posixです。
ESモジュールで__dirnameを使ってエラー
ESモジュールに__dirnameはありません。path.dirname(fileURLToPath(import.meta.url))で作ります。
カレントディレクトリ依存のパスで動かなくなる
相対パスは「実行したフォルダ」が基準になります。実行場所に左右されたくないなら、__dirnameを基準に絶対パスを作ります。
よくある質問
path.joinは断片を区切り文字で連結するだけで、結果は相対パスのままです。path.resolveは絶対パスを組み立て、すべて相対なら現在の作業フォルダを起点にします。確実な絶対パスがほしいときはresolveか__dirnameとの組み合わせを使います。path.joinに任せます。Windowsでは\\、Linuxでは/と、その環境に合った区切りを自動で使ってくれます。URLのパスを組み立てるときだけ、常に/のpath.posix.joinを使います。path.extname("report.txt")で.txtが得られます。拡張子を除いたファイル名はpath.basename(p, ".txt")やpath.parse(p).nameで取り出せます。path.join(__dirname, "ファイル名")で絶対パスを作ります。__dirnameはそのファイルがあるフォルダを指すため、どこから実行しても同じファイルを正しく読めます。ESモジュールではimport.meta.urlから作ります。__dirnameがありません。import { fileURLToPath } from "node:url"とimport { dirname } from "node:path"を使い、dirname(fileURLToPath(import.meta.url))で同じものを作ります。まとめ
- パスの連結は
path.joinに任せ、区切り文字を直接書きません。 joinは相対的な連結、resolveは絶対パスの組み立てです。- 取り出しは
basename・dirname・extname、まとめて分解はpath.parseです。 - ファイルの場所を基準にするなら
path.join(__dirname, "...")を使います。 - 区切り文字はOS依存(
path.sep)。常に/ならpath.posixです。 - ESモジュールでは
__dirnameの代わりにimport.meta.urlから作ります。
Node.jsのパス操作は、文字列結合をやめてpathモジュールに任せるだけで、OS差や区切りのバグから解放されます。joinとresolveの違い、そして__dirnameを基準にする習慣を押さえれば、どの環境でも安定して動くコードを書けます。

