Node.jsでアプリケーションを開発する際、ファイルやディレクトリのパス操作は避けて通れない重要な要素です。
特に標準モジュールの「path」で使用頻度が高いのが、path.joinとpath.resolveの2つのメソッドです。
これらはいずれも複数のパスセグメントを結合する役割を持ちますが、その内部動作と出力結果には明確な違いが存在します。
挙動の違いを正しく理解していないと、開発環境では動くのにデプロイ後の環境でファイルが見つからないといったバグを誘発する可能性があります。
本記事では、2026年現在のNode.js開発においても必須知識となる、これら2つのメソッドの使い分けと具体的な活用シーンを整理して紹介します。
path.join の基本動作と特徴
path.joinは、引数として渡されたすべてのパスセグメントを、プラットフォーム固有の区切り記号(Windowsなら「\」、POSIXなら「/」)を使用して結合します。
このメソッドの最大の特徴は、単純にパス文字列を連結し、結果を正規化するという点にあります。
「正規化」とは、パスに含まれる .(現在のディレクトリ)や ..(親ディレクトリ)を適切に処理して、冗長な部分を削ぎ落とすことを指します。
以下のコード例で、path.joinがどのようにパスを処理するかを確認してみましょう。
const path = require('path');
// 単純な結合
const path1 = path.join('users', 'admin', 'settings.json');
console.log('path1:', path1);
// ..(親ディレクトリ)を含む結合
const path2 = path.join('/usr', 'local', '..', 'bin');
console.log('path2:', path2);
// 空の文字列が含まれる場合
const path3 = path.join('app', '', 'config');
console.log('path3:', path3);
path1: users/admin/settings.json (Windows環境では users\admin\settings.json)
path2: /usr/bin
path3: app/config
path.joinは、引数に絶対パスが含まれていても、それを単なる文字列の一部として扱います。
例えば、途中の引数に / から始まるパスを渡しても、それより前の引数が無視されることはありません。
あくまで「与えられたパーツを順番につなげる」という挙動に徹しているのがpath.joinの性質です。
path.resolve の基本動作と特徴
一方でpath.resolveは、引数として渡されたパスのシーケンスを、絶対パスに解決(変換)するためのメソッドです。
このメソッドは、右から左に向かって引数を順番に処理し、絶対パスが構築されるまで遡って処理を続けます。
もしすべての引数を処理しても絶対パスにならない場合は、現在の作業ディレクトリ(Current Working Directory)が先頭に付与されます。
これは、ターミナルで cd コマンドを順番に実行していく様子をイメージすると理解しやすくなります。
const path = require('path');
// 現在の作業ディレクトリが /home/user/project の場合
const res1 = path.resolve('src', 'main.js');
console.log('res1:', res1);
// 引数に絶対パスが含まれる場合
const res2 = path.resolve('/etc', 'nginx', 'conf.d');
console.log('res2:', res2);
// 途中に絶対パスが含まれる場合(それより前は無視される)
const res3 = path.resolve('tmp', '/var', 'log');
console.log('res3:', res3);
res1: /home/user/project/src/main.js
res2: /etc/nginx/conf.d
res3: /var/log
path.resolveにおいて最も注意すべき点は、引数に / で始まる文字列が現れると、そこをルート(起点)とみなして、それ以前の引数を破棄するという挙動です。
また、最終的な出力は必ず絶対パスになるため、実行環境のディレクトリ構造に依存した結果が返されます。
path.join と path.resolve の決定的な違い
これら2つのメソッドの主な違いを理解するために、以下の表で比較してみましょう。
| 比較項目 | path.join | path.resolve |
|---|---|---|
| 主な役割 | パスセグメントの単純な結合と正規化 | 絶対パスの生成 |
| 戻り値の形式 | 相対パスまたは絶対パス(入力に依存) | 常に絶対パス |
| 先頭の / の扱い | 単なるパスの一部として扱う | ルートディレクトリとして扱う(リセット) |
| 作業ディレクトリの影響 | 受けない(入力された文字列のみで完結) | 受ける(絶対パスにならない場合に付与) |
同じ引数を与えた場合でも、結果が大きく異なるケースを具体的に見てみましょう。
const path = require('path');
const args = ['folder', '/subfolder', 'file.txt'];
console.log('join :', path.join(...args));
console.log('resolve:', path.resolve(...args));
join : folder/subfolder/file.txt
resolve: /subfolder/file.txt
path.joinはすべての引数を連結していますが、path.resolveは /subfolder が現れた時点で、その前にあった folder を切り捨てていることがわかります。
この仕様の違いが、バグの温床になりやすいため、コードを書く際には細心の注意が必要です。
絶対パスの扱いによる挙動の変化
path.joinは、入力された文字列の形をできるだけ維持しようとします。
引数の先頭に / があれば絶対パスのような形式で出力されますが、そうでなければ相対パスのまま出力されます。
対してpath.resolveは、いかなる場合も最終的に絶対パスを返すことを保証しようとします。
そのため、引数が空であっても、実行時のカレントディレクトリを返してくるという特性があります。
カレントディレクトリ(process.cwd)の影響
path.resolveは内部的に process.cwd() を参照しています。
これにより、スクリプトを実行する場所(ディレクトリ)が変わると、path.resolveの出力結果も変わってしまう可能性があることを覚えておきましょう。
一方、path.joinは純粋に文字列の加工を行うため、どのディレクトリからスクリプトを実行しても結果は変わりません。
実践的な使い分けシーン
挙動の違いがわかったところで、実務でどのように使い分けるべきかを整理します。
相対的なパスを構築したい場合は path.join
WebサイトのURLパスを生成したり、特定のディレクトリ構造を相対的に維持したままパスを組み立てたい場合には、path.joinが適しています。
例えば、テンプレートエンジンで共通のパーツを読み込む際のパス指定などがこれに該当します。
また、クラウドストレージ(S3など)のキーを生成する際も、ローカルマシンの絶対パスは不要なため、path.joinを使うのが一般的です。
ファイル操作などで絶対パスが必要な場合は path.resolve
fs.readFile や fs.writeFile などのファイルシステム操作を行う際は、path.resolveの使用を推奨します。
Node.jsの fs モジュールに相対パスを渡すと、それは「プログラムを実行した時のカレントディレクトリ」からの相対パスとして解釈されます。
プログラムを node index.js と実行するか、親ディレクトリから node project/index.js と実行するかで参照先が変わってしまうため、path.resolve を使って常に一意の絶対パスを指定するのが安全です。
__dirname との組み合わせ
Node.js(CommonJS)では、現在実行中のスクリプトがあるディレクトリを指す __dirname がよく使われます。
これと path.join を組み合わせる手法は、設定ファイルなどの読み込みで非常によく使われるパターンです。
// スクリプトと同じ階層にある config.json を安全に参照する
const configPath = path.join(__dirname, 'config.json');
__dirname 自体が絶対パスであるため、path.join を使っても結果的に絶対パスが得られます。
この場合、path.resolve を使っても同様の結果が得られますが、慣習として「起点が絶対パス(__dirname)だとわかっている場合は join を使う」という開発者も多く存在します。
開発で役立つTips
パス操作におけるトラブルを防ぐための、いくつかのテクニックを紹介します。
OS間の差異を吸収する
Node.jsの path モジュールは、実行されているOSを自動的に判別して動作します。
しかし、Windows上で動作させている時にPOSIX形式(スラッシュ区切り)のパスを強制したい場合などは、path.posix.join() のように明示的に指定することも可能です。
逆にWindows形式を強制したい場合は path.win32 を使用しますが、通常は path モジュールに任せるのが最も安全です。
ESモジュール(ESM)での対応
モダンなNode.js開発(ESモジュール)では、__dirname や __filename が標準では使用できません。
2026年現在の環境では、以下のように import.meta.url からパスを取得するのが標準的です。
import { fileURLToPath } from 'url';
import path from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const fullPath = path.resolve(__dirname, 'data', 'input.csv');
このようにESM環境でも path.resolve や path.join の重要性は変わらず、むしろパスの構築機会は増えています。
まとめ
Node.jsにおけるパス操作の要である path.join と path.resolve は、一見似ていますがその設計思想は大きく異なります。
path.join は「パスのパーツを正しく結合し、正規化されたパス文字列を作る」ための道具です。
一方、path.resolve は「バラバラの情報を統合して、システム上の完全な絶対パスを特定する」ための道具です。
特に path.resolve が引数の途中に / を見つけた際にそれまでの処理をリセットする挙動は、意図しないバグの原因になりやすいため、常に意識しておく必要があります。
ファイルシステムにアクセスする際は path.resolve や __dirname を活用して絶対パスを生成し、ディレクトリ構造の論理的な組み立てには path.join を使うという原則を守ることで、堅牢なNode.jsアプリケーションを構築できるでしょう。
日々のコーディングの中で、今作成しているパスが「単なる文字列」なのか「OSが直接参照する場所」なのかを問い直す習慣をつけることが大切です。
