JavaScript Tips | 文字列ユーティリティ:URL 系 - パス結合

JavaScript JavaScript
スポンサーリンク

「パス結合」で何を楽にしたいのか

「パス結合」は、URL のパスを安全にくっつけるユーティリティです。

"/api" と "users" を結合して "/api/users" にしたい
"/api/" と "/users" を結合しても "//" になってほしくない

こういう「/ の重複」や「/ の抜け」を、毎回自分で気にしたくないからこそ、
“パスをいい感じにくっつけてくれる関数”を持っておく価値があります。


よくある「パス結合の罠」を先に見ておく

素直に + でつなぐとこうなる

例えば、こんなコードを書きがちです。

const base = "/api";
const path = "/users";

const url = base + "/" + path;
// "/api//users" になってしまう
JavaScript

逆に、こう書くと今度は / が足りないこともあります。

const base = "/api/";
const path = "users";

const url = base + path;
// "/api/users"(このケースはたまたま正しい)
JavaScript

問題は、「base 側に / が付いているか」「path 側の先頭に / があるか」を
呼び出し側が毎回意識しないといけないことです。

これをユーティリティに任せてしまうのが「パス結合」です。


一番基本的なパス結合ユーティリティ

シンプル版 joinPath の実装

まずは、「複数のパス断片を受け取って、1 本のパスにする」関数を作ります。

function joinPath(...parts) {
  const filtered = parts
    .filter((p) => p != null && p !== "")
    .map((p) => String(p));

  if (filtered.length === 0) {
    return "";
  }

  const normalized = filtered.map((p, index) => {
    let s = p;

    if (index === 0) {
      s = s.replace(/\/+$/g, "");
    } else {
      s = s.replace(/^\/+/g, "").replace(/\/+$/g, "");
    }

    return s;
  });

  let result = normalized.join("/");

  if (!result.startsWith("/")) {
    result = "/" + result;
  }

  return result;
}
JavaScript

重要なポイントをかみ砕いて説明する

ここはしっかり理解してほしいところです。

1. null / 空文字は無視する

const filtered = parts
  .filter((p) => p != null && p !== "")
  .map((p) => String(p));
JavaScript

nullundefined、空文字が混ざっていても、
「なかったこと」にして結合できるようにしています。

joinPath("/api", null, "users");
// "/api/users"
JavaScript

こうしておくと、「条件によってパスを足したり引いたり」するときに扱いやすくなります。

2. 先頭の要素とそれ以外でルールを変える

const normalized = filtered.map((p, index) => {
  let s = p;

  if (index === 0) {
    s = s.replace(/\/+$/g, "");
  } else {
    s = s.replace(/^\/+/g, "").replace(/\/+$/g, "");
  }

  return s;
});
JavaScript

ここが一番の肝です。

先頭の要素(index === 0)
→ 末尾の / だけ消す(先頭の / は残す)

2 個目以降の要素
→ 先頭と末尾の / を両方消す

こうすることで、例えば次のような入力が全部きれいに揃います。

joinPath("/api", "users");        // "/api/users"
joinPath("/api/", "users");       // "/api/users"
joinPath("/api", "/users");       // "/api/users"
joinPath("/api/", "/users/");     // "/api/users"
JavaScript

「先頭のパスは / から始まっていてほしい」
「途中のパスは / なしでつなぎたい」
というルールを、ここで強制しています。

3. 最後に / で join して、先頭 / を保証する

let result = normalized.join("/");

if (!result.startsWith("/")) {
  result = "/" + result;
}
JavaScript

join("/")["/api", "users"]"/api/users" のように結合します。

さらに、もし先頭に / がなければ付けておきます。
これで、常に「絶対パス」の形(/xxx/yyy)で返ってくるようになります。


実際の動きを例で確認する

よくあるパターンを全部試してみる

joinPath("/api", "users");
// "/api/users"

joinPath("/api/", "users");
// "/api/users"

joinPath("/api", "/users");
// "/api/users"

joinPath("/api/", "/users/");
// "/api/users"

joinPath("/api", "v1", "users", "123");
// "/api/v1/users/123"

joinPath("/api/", null, "users", "", "123/");
// "/api/users/123"
JavaScript

どんな組み合わせでも、「/ の重複なし」「末尾に余計な / なし」で揃っているのが分かると思います。


ベース URL と組み合わせるときの考え方

ドメイン込みの URL にも使いたい場合

さっきの joinPath は、「パス部分」だけを想定していました。

joinPath("/api", "users"); // "/api/users"
JavaScript

もし、https://example.com のようなベース URL と組み合わせたい場合は、
「ドメイン部分」と「パス部分」を分けて考えるときれいです。

function joinUrl(baseUrl, ...paths) {
  const base = String(baseUrl).replace(/\/+$/g, "");
  const path = joinPath(...paths);

  return base + path;
}
JavaScript

使い方はこうです。

joinUrl("https://example.com", "api", "users");
// "https://example.com/api/users"

joinUrl("https://example.com/", "/api/", "/users/");
// "https://example.com/api/users"
JavaScript

ここでのポイントは、

  • ベース URL の末尾の / は消しておく
  • パス部分は joinPath に任せる

という責務分担です。


実務での使いどころと設計のポイント

「呼び出し側に / の有無を意識させない」

パス結合ユーティリティの一番の価値は、
「呼び出し側が / の有無を気にしなくてよくなる」ことです。

// 悪い例:呼び出し側が / を意識し続ける
const url = "/api/" + userId + "/profile";

// 良い例:ユーティリティに任せる
const url = joinPath("/api", userId, "profile");
JavaScript

後者の方が、

  • 読みやすい
  • / の付け忘れ・付けすぎが起きない
  • パスの構造が一目で分かる

というメリットがあります。

「パス結合」と「クエリ生成」を分けて考える

URL 全体は、ざっくりこう分けて考えると整理しやすいです。

  • パス部分 → joinPath / joinUrl
  • クエリ部分 → toQueryString(前にやったやつ)
const path = joinPath("/api", "users", userId);
const qs = toQueryString({ include: "profile" });

const url = path + qs;
// "/api/users/123?include=profile"
JavaScript

こうしておくと、

  • パスの構造を変えたいときは joinPath の引数を変える
  • クエリの中身を変えたいときは toQueryString の引数を変える

という、きれいな責務分担になります。


ちょっとだけ手を動かしてみる

コンソールで、次のあたりを試してみてください。

joinPath("/api", "users");
joinPath("/api/", "users");
joinPath("/api", "/users");
joinPath("/api/", "/users/");

joinUrl("https://example.com", "api", "users", "123");
joinUrl("https://example.com/", "/api/", "/users/");
JavaScript

「どんな入力でも同じ形に整えてくれる感覚」を、自分の目で確かめてみてください。

そのうえで、自分のプロジェクトに

export function joinPath(...parts) { ... }
export function joinUrl(baseUrl, ...paths) { ... }
JavaScript

を置いて、

  • パスだけ結合したい → joinPath
  • ドメイン込みで URL を作りたい → joinUrl

という使い分けをルール化してみてください。

それだけで、あなたの URL 組み立てコードは、
場当たり的な文字列連結から、読みやすくて安全な“業務レベルのパス結合ユーティリティ”に一段レベルアップします。

タイトルとURLをコピーしました