何をしたいユーティリティか:「パス正規化」
ここでの「パス正規化」は、"//api//v1/../v2//users/./123/" のような「ぐちゃっとしたパス文字列」を、"/api/v2/users/123" のような「きれいで一貫した形」に整える処理です。
ファイルパスでも URL パスでも、
「余計なスラッシュ」「. や ..」「末尾スラッシュの有無」がバラバラだと、
比較やログ出力、ルーティング判定がやりづらくなります。
だからこそ、
「パスを使う前に必ず正規化関数を通す」というルールを作るのが、業務ではかなり効きます。
パス正規化でやりたいことを整理する
どんな“ゆがみ”を直したいか
パス文字列には、よくこんな「ゆがみ」が混ざります。
// のような連続スラッシュ/./ のような「カレントディレクトリ」/a/b/../c のような「親ディレクトリへの戻り」
末尾の余計な /
これらを、次のようなルールで整えます。
連続スラッシュは 1 個にまとめる。. は無視する。.. は一つ前のセグメントを削る。
先頭の / は維持し、末尾の / は基本的に削る(必要ならオプション)。
この「ルールセット」をコードに落としたものが、パス正規化ユーティリティです。
シンプルなパス正規化の実装
normalizePath のコード
まずは、UNIX 風のパス(/ 区切り)を対象にしたシンプル版を書きます。
function normalizePath(path) {
if (!path) {
return "/";
}
const isAbsolute = path.startsWith("/");
const rawSegments = path.split("/");
const segments = [];
for (const seg of rawSegments) {
if (!seg || seg === ".") {
continue;
}
if (seg === "..") {
if (segments.length > 0 && segments[segments.length - 1] !== "..") {
segments.pop();
} else if (!isAbsolute) {
segments.push("..");
}
continue;
}
segments.push(seg);
}
let normalized = segments.join("/");
if (isAbsolute) {
normalized = "/" + normalized;
}
if (normalized === "") {
normalized = isAbsolute ? "/" : ".";
}
return normalized;
}
JavaScript重要なポイントを一つずつかみ砕く
絶対パスか相対パスかを最初に判定する
const isAbsolute = path.startsWith("/");
JavaScript"/api/users" のように先頭が / なら「絶対パス」、"api/users" のように先頭に / がなければ「相対パス」とみなします。
この情報は、最後に「先頭に / を付けるかどうか」を決めるために使います。
スラッシュで分割して「セグメント」に分ける
const rawSegments = path.split("/");
JavaScript"/api//v1/../users/./123/" は、分割するとこういう配列になります。
["", "api", "", "v1", "..", "users", ".", "123", ""]
空文字が混ざっているのは、
先頭の / や連続スラッシュ // の影響です。
この「生のセグメント配列」を、ルールに従って整えていきます。
. と空文字は無視する
if (!seg || seg === ".") {
continue;
}
JavaScript"." は「今のディレクトリ」を意味しますが、
パスの正規化では「無視してよいもの」として扱います。
空文字("")は、先頭の / や連続スラッシュから生まれたものなので、
これも無視します。
.. は「一つ前のセグメントを削る」
if (seg === "..") {
if (segments.length > 0 && segments[segments.length - 1] !== "..") {
segments.pop();
} else if (!isAbsolute) {
segments.push("..");
}
continue;
}
JavaScriptここが一番大事な部分です。
.. は「親ディレクトリ」を意味するので、
すでに積み上げたセグメントがあれば、それを 1 つ削ります。
例えば、
["api", "v1"] に .. が来たら → ["api"] に戻す。
ただし、絶対パスの先頭より外には出られないので、
絶対パスの場合は「もう削るものがない」状態では何もしません。
相対パスの場合は、"../../foo" のような形を保ちたいこともあるので、
削るものがなければ .. 自体を残します。
残りは普通のセグメントとして積む
segments.push(seg);
JavaScript. でも .. でもない普通の文字列は、
そのまま「パスの一部」として積み上げます。
こうしてループを回し終わると、segments には「余計なものを取り除いたパスの部品」が並びます。
最後に文字列に戻す
let normalized = segments.join("/");
if (isAbsolute) {
normalized = "/" + normalized;
}
if (normalized === "") {
normalized = isAbsolute ? "/" : ".";
}
JavaScriptセグメントを / でつなぎ直し、
絶対パスなら先頭に / を付けます。
もしセグメントが空([])なら、
絶対パスなら /、相対パスなら "."(カレントディレクトリ)を返すようにしています。
実際の動きを例で確認する
normalizePath("/api//v1/../users/./123/");
// "/api/users/123"
normalizePath("api/./v1//users/../");
// "api/v1"
normalizePath("/a/b/../../c");
// "/c"
normalizePath("a/../../b");
// "../b"
normalizePath("/");
// "/"
normalizePath("");
// "/"
JavaScript特に注目してほしいのは、.. の扱いです。
"/a/b/../../c" → "/c""a/../../b" → "../b"
絶対パスでは「ルートより上には行かない」、
相対パスでは「.. を残すこともある」という違いが出ています。
URL パス用に少しだけルールを変える
末尾スラッシュを残したい場合
API の設計やルーティングによっては、"/api/users" と "/api/users/" を区別したいこともあります。
その場合は、「末尾スラッシュを残すかどうか」をオプションにできます。
function normalizePathUrl(path, options = {}) {
const { keepTrailingSlash = false } = options;
const originalHasTrailingSlash = path?.endsWith("/");
let normalized = normalizePath(path);
if (keepTrailingSlash && originalHasTrailingSlash && !normalized.endsWith("/")) {
normalized += "/";
}
return normalized;
}
JavaScriptこれで、
normalizePathUrl("/api/users/");
// "/api/users"
normalizePathUrl("/api/users/", { keepTrailingSlash: true });
// "/api/users/"
JavaScriptのように、用途に応じて挙動を変えられます。
実務で意識してほしい設計のポイント
「パスを比較する前に必ず正規化する」
ルーティングや権限チェックで、
if (path === "/api/users") { ... }
のような比較をするとき、
入力側のパスが "/api//users/" のように微妙に違っていると、
本来同じ意味なのに一致しません。
そこで、
const normalized = normalizePath(inputPath);
if (normalized === "/api/users") {
...
}
JavaScriptという形にしておけば、
「見た目が違うだけで意味は同じ」パスを、同一視できます。
ログやメトリクスでも「正規化済み」を使う
ログやメトリクスでパスをキーにするときも、
正規化しておかないと、似たようなパスがバラバラに集計されてしまいます。
"/api/users" と "/api/users/" と "/api//users" が別々にカウントされる、
というのはよくある「もったいないパターン」です。
「ログに出す前に normalizePath を通す」というルールを作るだけで、
集計の精度がかなり上がります。
OS のパスと URL のパスを混同しない
ここで書いたのは「UNIX 風の / 区切りパス」の正規化です。
Windows のファイルパス(C:\path\to\file)や、
URL 全体(https://example.com/api/users)を扱う場合は、
別途ルールを決める必要があります。
ただ、「セグメントに分けて . と .. を処理する」という考え方自体は同じなので、
このユーティリティをベースに、用途ごとのバリエーションを作るのが現実的です。
少し手を動かして感覚をつかむ
コンソールで、次のようなコードを実際に打ってみてください。
normalizePath("/api//v1/../users/./123/");
normalizePath("api/./v1//users/../");
normalizePath("/a/b/../../c");
normalizePath("a/../../b");
normalizePathUrl("/api/users/");
normalizePathUrl("/api/users/", { keepTrailingSlash: true });
JavaScript返ってきた文字列を見て、
「自分が頭の中でイメージした“きれいなパス”と一致しているか」
「.. の挙動が直感と合っているか」
を確認してみてください。
そのうえで、自分のプロジェクトに
export function normalizePath(...) { ... }
export function normalizePathUrl(...) { ... }
JavaScriptを置き、
「パスを扱いたくなったら、まず“パス正規化ユーティリティ”を通す」
というルールを作ってみてください。
それだけで、あなたのシステムのパス処理は、場当たり的な文字列操作から、
意図と一貫性を備えた「業務レベルのパス設計」に変わっていきます。
