ウェブエンジニア問題集
第7章

URL設計とルーティング — リソース指向のパス設計

13
この章の目次開く

URLはAPIやWebアプリの「住所」です。一度公開したURLは、外部サービスからのリンク・ブックマーク・検索エンジンのインデックスに刻まれ、後から変更するコストが非常に高くなります。

つまりURLは、最初の設計がそのまま長く残る部類の成果物です。この章では、前章のAPI設計の基本で扱ったREST原則を前提に、URLそのものの設計——パスの構造、ネストの深さ、パラメータの使い分け——を掘り下げます。

学習者学習者

URLって、動けば何でもいい気がしちゃう…。/getUserOrders?type=2 でもデータは取れるよね?

先生先生

動くことと設計が良いことは別問題。そのURLだと「何のリソースを扱っているのか」「type=2 が何なのか」が読み取れない。URLは利用者が最初に触れるインターフェースだから、URLだけで意味が伝わる状態を目指すんだ。

URLの構成要素

まず用語を整理します。URLは次のパーツで構成されます。

https://api.example.com/users/123/orders?status=paid&page=2#section
└─┬─┘   └──────┬──────┘└───────┬───────┘└────────┬────────┘└──┬──┘
スキーム      ホスト           パス           クエリ        フラグメント
構成要素役割
スキームhttps通信プロトコル
ホストapi.example.comサーバーの場所
パス/users/123/ordersリソースの特定(階層構造)
クエリ?status=paid&page=2リソースの絞り込み・加工(key=value の組)
フラグメント#sectionページ内の位置(サーバーには送信されない)

URL設計で主に考えるのはパスクエリの2つです。この2つの役割分担が、この章の中心テーマになります。

リソース指向のパス設計

RESTの考え方では、パスは「リソース(データの単位)」を表し、操作はHTTPメソッドで表現します。パス設計の基本ルールは次の3つです。

  1. 名詞を使う —— 操作(動詞)はメソッドに任せる
  2. 複数形で統一する —— /users/products のようにコレクション名は複数形にする
  3. コレクションとメンバーの2段構え —— 一覧は /users、個別は /users/123
⭕ 良い例
GET    /users          — ユーザー一覧(コレクション)
GET    /users/123      — ID 123のユーザー(メンバー)
POST   /users          — ユーザー作成
DELETE /users/123      — ユーザー削除

❌ 避ける例
GET  /getUsers          — 動詞が入っている
GET  /user/123          — 単数形と複数形が混在する原因になる
POST /users/create      — 操作がパスに含まれている(POSTと重複)
パスは「何を」を表し、HTTPメソッドは「どうする」を表す——この分担を崩さないことがリソース指向の核心です。

ネストの深さ — 親子関係をどこまでパスに映すか

リソースには親子関係があります。「ユーザーの注文」「注文の明細」のような関係は、パスのネストで表現できます。

GET /users/123/orders        — ユーザー123の注文一覧
GET /users/123/orders/456    — ユーザー123の注文456

ただし、ネストは深くなるほど扱いにくくなります。

❌ 深すぎるネスト
GET /users/123/orders/456/items/789/reviews/12

深いネストには次の問題があります。

  • URLを組み立てるために、利用側が親のIDをすべて知っている必要がある(注文明細を見たいだけなのにユーザーIDが必要)
  • 同じリソースに複数のURLができやすい(/orders/456/users/123/orders/456 が併存し、どちらが正か曖昧になる)
  • パスが長くなり、ログやデバッグで読みにくい

実務の目安 — ネストは1階層まで

ネストは「親/親ID/子」の1階層までにとどめ、孫リソースは独立したパスで直接参照できるようにするのが実務的な落とし所です。

⭕ 浅く保つ設計
GET /users/123/orders    — 「ユーザーの注文一覧」は関係が本質なのでネストでよい
GET /orders/456          — 注文自体はIDで一意なので、直接参照できるようにする
GET /orders/456/items    — 明細は「注文の子」として1階層のネスト
GET /items/789           — 明細も直接参照可能にする

ネストを使うかどうかの判断基準は「子リソースが親なしで意味を持つか」です。注文はユーザーと独立に(管理画面などで)参照される場面が多いため直接参照を用意します。一方、注文明細のように親と常にセットで扱うリソースは、ネスト経由だけでも困りません。

パスパラメータとクエリパラメータの使い分け

学習者学習者

/users/123 はパスにIDを入れるのに、検索条件は ?keyword=... でクエリに入れる…。この使い分け、毎回どっちにするか迷うんだよね。

迷ったときの判断基準はシンプルです。

リソースを「特定」するものはパスに、リソースの集合を「加工」(絞り込み・並び替え・ページ分割)するものはクエリに置く。
パラメータ位置性質
パスパラメータパスの一部必須。無いとリソースが特定できない/users/123/posts/my-first-post
クエリパラメータ? 以降省略可能。無くてもデフォルトの結果が返る?status=paid?sort=-createdAt?page=2

クエリパラメータの典型的な用途は次の3つです。

絞り込み(フィルタ):  GET /orders?status=paid&userId=123
並び替え(ソート):    GET /products?sort=-price        (-で降順を表す例)
ページネーション:      GET /articles?page=3&perPage=20

「クエリは省略できる」という性質は設計の検算にも使えます。GET /orders?status=paid から ?status=paid を外しても「全注文の一覧」として意味が通ります。逆に /users/123 から 123 を外すと対象が決まりません。外して意味が通るならクエリ、通らないならパスです。

URLに何を使うか — 連番ID・UUID・スラッグ

メンバーを特定する /users/123 の「123」に何を使うかも設計判断です。

識別子長所短所
連番ID/users/123短い・DBの主キーをそのまま使える総数や登録順が推測できる。連番を辿る攻撃の足がかりになる
UUID / ULID/users/01J5X…推測不可能。分散環境でも衝突しない長くて人間が読めない
スラッグ/articles/how-to-design-url人間が読める。SEOに有利一意性の管理とリネーム時のリダイレクトが必要

使い分けの目安は次のとおりです。

  • 管理画面・内部API — 連番IDかUUID。人間が読む必要が薄いため
  • 公開コンテンツ(記事・商品ページなど) — スラッグ。URLから内容が想像でき、検索結果でもクリックされやすい
  • 推測されると困るリソース(請求書・招待リンクなど) — UUIDなどの推測不可能な識別子

CRUDに収まらない操作をどう表すか

「注文をキャンセルする」「記事を公開する」のような操作は、GET/POST/PUT/DELETEに素直に対応しません。実務では主に2つの表現方法があります。

方法1: 状態の更新として表現する

キャンセルを「注文の status フィールドの更新」と捉え、PATCHで表現します。

PATCH /orders/456
{ "status": "cancelled" }

リソース指向としては最も素直ですが、「キャンセル時はメール送信もする」のような単なるフィールド更新を超えた副作用があると、PATCHの意味から離れていきます。

方法2: 操作をサブリソースとして切り出す

操作自体を名詞化(キャンセル=cancellation)するか、慣例的に動詞のサブパスを許容します。

POST /orders/456/cancellation    — 「キャンセル」というリソースの作成と捉える
POST /orders/456/cancel          — 実務ではこの形も広く使われる
原則はリソース指向を守りつつ、収まらない操作は「POST + サブパス」で例外として明示する——例外の書式をプロジェクト内で統一しておくことが重要です。
ルールの枠を越える場面
原則に収まらないケースは必ず出る。例外の扱い方を先に決めておく

画面のURL設計 — ページのルーティング

ここまでAPIのURLを中心に見てきましたが、フロントエンドのページURLも同じ考え方で設計します。

⭕ ページURLもリソース指向で
/products               — 商品一覧ページ
/products/123           — 商品詳細ページ
/products?category=book&page=2   — カテゴリ絞り込み+ページ番号

ページURL特有の観点として、**「URLは共有・復元できる状態である」**ことを意識します。

  • 検索条件やページ番号をURL(クエリ)に反映しておくと、リロードしても状態が復元され、URLを共有すれば同じ画面を他人が開ける
  • 逆に、絞り込み条件をJavaScriptのメモリ上だけに持つと、リロードで消え、共有もできない

どの状態をURLに載せるべきかは、状態管理の設計で扱う「URLと状態の関係」の論点そのものです。URL設計は、状態管理設計の一部でもあります。

よくあるハマりどころ

公開後のURL変更

冒頭で触れたとおり、URLの変更は「外部からのリンクをすべて壊す」行為です。避けられない場合は、旧URLから新URLへのリダイレクト(301 Moved Permanently) を必ず用意します。「変更しやすい設計」より「変更しなくて済む設計」を最初に狙うのがURL設計の特徴です。

大文字・小文字、区切り文字の混在

/userProfile/user-profile が混在すると、利用者はエンドポイントごとに正しい表記を確認する羽目になります。URLのパスは小文字 + ハイフン区切り(ケバブケース) に統一するのが一般的な慣例です(アンダースコアより可読性が高く、検索エンジンも単語の区切りとして認識します)。

何でもクエリパラメータに詰め込む

?action=delete&target=user&id=123 のように操作までクエリで表現し始めると、URLからリソース構造が消えてRESTの利点(メソッドとパスから動作が予測できること)が失われます。GETで削除が実行される設計は、クローラーのアクセスでデータが消える事故にも直結します。

学習者学習者

GETで削除って、リンクを開いただけでデータが消えるってこと…?そんな事故、本当に起きるの?

先生先生

起きる。GETリンクで削除を実装していたWebアプリで、クローラーやブラウザのリンク先読み機能がリンクを片っ端から辿って、コンテンツが大量に消えた事例は昔から繰り返し報告されているよ。「GETは安全(副作用なし)」というHTTPの約束は、ブラウザや検索エンジンがその前提で動くから破ってはいけないんだ。

ちゃんと使うためのポイント

  • URLは公開した瞬間から変更コストが跳ね上がる——最初の設計に時間をかける価値がある
  • パスは名詞・複数形で統一し、操作はHTTPメソッドで表現する
  • ネストは1階層まで。子が親なしで意味を持つなら直接参照のパスを用意する
  • リソースの特定はパス、絞り込み・並び替え・ページ分割はクエリ——「外して意味が通るか」で検算する
  • 識別子は用途で選ぶ:内部は連番/UUID、公開コンテンツはスラッグ、推測されると困るものはUUID
  • CRUDに収まらない操作は「POST + サブパス」で統一的に例外扱いする
  • ページURLも設計対象。共有・リロードで復元したい状態はURLに載せる

次の章では、URLで特定したリソースを「誰に・どこまで見せるか」を決める認証と認可の設計認証・認可の設計)を解説します。

参考リンク