リソース指向アーキテクチャ(ROA)とは?RESTとの違いとAPI設計への落とし込み方
リソース指向アーキテクチャ(ROA)は、RESTというアーキテクチャスタイルを「実際にWeb APIを設計するときの手順」に落とし込んだ設計指針です。RESTの論文を読んでも具体的なURLの決め方までは書かれていないため、その空白を埋める形で提唱されました。ここではROAの定義と4つの特性を出発点に、リソースの切り出し方、URI命名、HTTPメソッドと冪等性、標準メソッドで表せない操作の扱い、エラー応答の標準まで、設計判断として使える形で整理します。
まとめ:リソース指向アーキテクチャの要点
- ROAはRESTの実装ガイドライン。RESTはFieldingの2000年の博士論文が定義したアーキテクチャスタイル、ROAはRichardson と Ruby の『RESTful Web Services』(2007)第4章が示した設計指針で、RFCのような標準仕様ではありません。
- ROAの4つの概念はリソース/URI(名前)/表現/リンク、4つの特性はアドレス可能性・ステートレス性・接続性・統一インターフェース。
- 設計は名詞(リソース)を先に決め、動詞はHTTPメソッドに任せる。URIに動詞を入れた時点でROAから外れます。
- GETとHEADは安全かつ冪等、PUTとDELETEは冪等、POSTもPATCHも冪等ではありません(RFC 9110 / RFC 5789)。POSTの再送対策は
Idempotency-Keyが事実上の標準です。 - 標準メソッドで表せない操作はAIP-136のカスタムメソッド(コロン構文)に隔離し、エラー応答はRFC 9457のProblem Detailsに寄せます。
- すべてのAPIをROAにする必要はありません。手続き的な操作が主のAPIは、無理にリソース化するより別方式が適します。
リソース指向アーキテクチャ(ROA)の定義
RESTとROAの役割分担
RESTはRoy Fieldingが2000年の博士論文第5章で定義したアーキテクチャスタイルで、クライアントサーバ・ステートレス・キャッシュ・統一インターフェース・階層化システム・コードオンデマンド(任意)という6つの制約からなります。これは制約の集合であり、「注文APIのURLをどう決めるか」までは指定しません。この抽象度の高さが、現場で「RESTfulのつもりだが人によって設計が違う」という状態を生みました。
Leonard Richardson と Sam Ruby は『RESTful Web Services』(2007)の第4章でリソース指向アーキテクチャを提示し、RESTの制約を守るための具体的な設計手順として整理しています。RESTが原則、ROAがその適用手順という関係です。ただしROAはRFCやW3C勧告のような標準仕様ではありません。準拠を主張できる適合性テストは存在せず、「ROA準拠」という表現に規格上の裏付けはない点は押さえておいてください。
ROAの4つの概念と4つの特性
ROAはAPIを次の4つの概念で捉えます。
| 概念 | 意味 | 具体例 |
|---|---|---|
| リソース | 名前を付けて公開する価値のあるもの | ある1件の注文、注文の一覧 |
| URI(名前) | リソースを一意に識別するアドレス | /v1/orders/1001 |
| 表現 | リソースの状態を返す形式 | JSON、XML、CSV |
| リンク | 次に辿れるリソースへの参照 | 注文から顧客リソースへのURI |
そのうえで、設計が満たすべき性質として4つの特性を挙げます。アドレス可能性は、公開したい情報のすべてがURIを持つこと。フィルタ後の一覧にもURIが振られていれば、その状態をそのままリンクとして共有できます。ステートレス性は、各リクエストが単体で処理に必要な情報を持つこと。サーバがクライアントの前回操作を覚えている前提のAPIは、この時点で外れます。接続性は、表現の中に次のリソースへのリンクを含めること(HATEOASに相当)。統一インターフェースは、操作をHTTP標準メソッドの意味どおりに使うことです。
実務で最も抜けやすいのは接続性です。IDだけを返して「URLはクライアント側で組み立ててください」という設計は広く使われていますが、ROAの定義上は接続性を満たしません。外す判断自体はありえます。ただし意図して外していることは設計として明示しておくべき箇所です。
リチャードソン成熟度モデルによる現在地の測定
自分のAPIがどこまでリソース指向かは、リチャードソン成熟度モデル(RMM)で位置づけられます。Leonard Richardsonが2008年のQCon講演で示し、Martin Fowlerが2010年3月18日の記事で広めたものです。
| レベル | 状態 | 典型的な症状 |
|---|---|---|
| 0 | HTTPを通信路として使うだけ | 単一URLに全操作をPOST |
| 1 | リソースの導入 | URIは分かれたがPOST一辺倒 |
| 2 | HTTPメソッドとステータスコードの正用 | 多くの実用APIがここ |
| 3 | ハイパーメディア制御(HATEOAS) | 応答に次操作のリンクを含む |
Fowler自身が記事で断っているとおり、RMMは「RESTのレベル」を定義したものではありません。彼はレベル3をFieldingの言うRESTの前提条件と位置づけつつ、モデル自体は概念を理解するための道具だとしています。「レベル3でなければRESTではない」という断定は原典を超えた読み方です。実務ではレベル2で止め、接続性が効く箇所(ページネーションの次ページリンクなど)にだけリンクを入れる選択が現実的です。
リソースの切り出しとURI命名(エンドポイント設計の手順)
リソース抽出の起点:DBスキーマの写し取りという失敗
リソース指向の設計は、機能一覧ではなく名詞の抽出から始めます。「注文を確定する」「注文をキャンセルする」という機能から入ると動詞ベースのURIになりますが、「注文」という名詞から入れば、確定もキャンセルも注文リソースの状態遷移として表現できます。
陥りやすいのは、DBのテーブル構成をそのままエンドポイントに写す設計です。GoogleのAPI設計指針AIP-121は「リソース指向APIは必ずしもデータベースではない」と明記し、内部スキーマの露出を戒めています。中間テーブルがそのまま/v1/user_role_mappings として公開されるようなAPIは、内部構造を変えた瞬間に破壊的変更になります。公開するのはクライアントが名前で呼びたいものだけです。
コレクションと個別リソースのURI命名規則
URIはコレクションと個別リソースが交互に現れる階層で構成します。
GET /v1/orders 注文コレクション
GET /v1/orders/1001 個別の注文
GET /v1/orders/1001/items 注文に属する明細コレクション
POST /v1/orders 注文の作成
命名の判断基準は3点です。コレクション名は複数形の名詞にする。URIに動詞を入れない(/getOrder や /orders/1001/cancel はROAから外れます)。階層を深くしすぎない。3階層を超えたら、そのリソースが本当に親に従属しているかを疑い、独立したトップレベルのコレクションに切り出すほうが扱いやすくなります。
標準メソッドの使い分けと冪等性
RFCに基づく安全性・冪等性の分類
統一インターフェースの実体は、HTTPメソッドの意味論をRFCどおりに使うことです。分類の出典はRFC 9110(HTTP Semantics、2022年6月発行。RFC 7231を廃止)で、PATCHのみRFC 5789が定義元になります。
| メソッド | 安全 | 冪等 | 出典 | ROAでの役割 |
|---|---|---|---|---|
| GET | Yes | Yes | RFC 9110 | リソースの取得 |
| HEAD | Yes | Yes | RFC 9110 | メタ情報のみ取得 |
| POST | No | No | RFC 9110 | コレクションへの作成 |
| PUT | No | Yes | RFC 9110 | 全置換 |
| DELETE | No | Yes | RFC 9110 | 削除 |
| PATCH | No | No | RFC 5789 | 部分更新 |
誤解が多いのがPATCHです。PATCHはRFC 9110の定義対象に含まれていません。定義元はRFC 5789(2010年3月)で、同RFCは「PATCHは安全でも冪等でもない」と明記しています。同時に、差分の表現の仕方によっては冪等なPATCHリクエストを発行しうるとも述べており(絶対値を設定する差分などがこれに当たります)、さらに別途、更新の衝突を防ぐためETagとIf-Matchによる条件付きリクエストを推奨しています。この2つは別の話です。条件付きリクエストは更新の取りこぼしを防ぐ仕組みであって、再送を冪等にする仕組みではありません(成功後の再送は412で弾かれます)。「PATCHは冪等」と書かれた解説を鵜呑みにせず、差分の中身で判断してください。
更新にPUTとPATCHのどちらを使うかは、GoogleのAIP-134が判断材料になります。AIP-134はUpdateにPATCH(部分更新)を推奨し、PUTによる全置換を強く非推奨としています。全置換は、クライアントが知らない新規フィールドを送らなかっただけで意図せず消してしまうためです。
ステータスコードの使い分け
統一インターフェースはメソッドとステータスコードの両輪です。リソース指向のAPIで頻出するのは次の組み合わせです。
| コード | 意味 | 返す場面 |
|---|---|---|
| 200 | OK | GET成功、本文を返す更新 |
| 201 | Created | POSTでリソース作成(Locationに新URI) |
| 204 | No Content | DELETE成功、本文なしの更新 |
| 400 | Bad Request | 構文・型が不正 |
| 404 | Not Found | リソースが存在しない |
| 409 | Conflict | 一意制約違反、状態遷移の矛盾 |
| 422 | Unprocessable Content | 構文は正しいが業務ルール違反 |
迷いやすいのが400と422の線引きです。JSONとして壊れている・型が違うなら400、JSONとしては正しいが「発送済みの注文はキャンセルできない」のような業務ルールに反するなら422、と決めておくとクライアント側のリトライ判断が機械的になります。どちらも再送しても結果は変わらないため、クライアントは再試行せずに入力を直すべきエラーです。
POST再送への対策:Idempotency-Key
POSTは冪等でないため、ネットワークタイムアウト後にクライアントが再送すると注文が二重に作られます。この対策として広く使われているのがIdempotency-Keyヘッダです。クライアントがリクエストごとにUUID等の一意なキーを付け、サーバは初回の結果を保存して、同じキーの再送には同じ応答を返します。
これはRFCではありません。IETF httpapi WGでドラフト(draft-ietf-httpapi-idempotency-key-header)として議論されていますが、2026年7月時点で最新は-07(2025年10月15日)、期限切れ状態のままRFC化されていません。拠り所になるのはStripeのようなデファクト仕様です。Stripeは最大255文字のキーを受け取り、初回のステータスコードとレスポンスボディを最低24時間保存して再送に同じ結果を返し、同一キーでパラメータが異なる場合はエラーにします。自前実装でも、この「保存期間」と「パラメータ不一致の扱い」を決めずに始めると運用で破綻します。
標準メソッドで表せない操作の逃がし方
カスタムメソッド:コロン構文による動詞の隔離
「メールを送信する」「動画をエンコードする」のように、名詞に還元しづらい操作は必ず出てきます。ここでURIに/orders/1001/cancelと動詞を生やすと、リソース階層に動詞が混ざり、以後どこまでが名詞でどこからが動詞か分からなくなります。
AIP-136はこの逃げ道として、リソースURIの末尾にコロンで動詞を付けるカスタムメソッドを定めています。副作用があるならPOST、状態の参照だけならGETを使い、新しいHTTPメソッドは定義しません。
POST /v1/publishers/123/books/456:archive
POST /v1/orders/1001:cancel
コロンで区切ることで、パスの階層構造(名詞)と操作(動詞)が構文上はっきり分かれます。一方でAIP-136は、カスタムメソッドは標準メソッドで容易に表現できない機能に限って使うべきとし、可能なら標準メソッドを優先するよう求めています(SHOULD)。カスタムメソッドが増え続けるAPIは、リソースの切り出しが間違っているサインです。「注文をキャンセルする」は、実はstatusフィールドへのPATCHで足りることが少なくありません。
非同期処理:オペレーションのリソース化(LRO)
数分かかる処理を同期で待たせると、タイムアウトとリトライで処理が多重に走ります。ROAらしい解き方は処理そのものをリソースにすることです。AIP-151はこれをロングランニングオペレーションとして定式化しています。作成リクエストは即座にオペレーションを返し、クライアントはそのURIをポーリングして完了を判定します。オペレーションはname、完了フラグのdone、成功時のresponse、失敗時のerrorを持ちます。
POST /v1/videos/42:transcode
-> 202 Accepted
{ "name": "operations/abc123", "done": false }
GET /v1/operations/abc123
-> 200 OK
{ "name": "operations/abc123", "done": true, "response": { ... } }
なお、AIP-151が定めるのはオペレーションリソースの構造であり、HTTPステータスコードまでは規定していません。上の例の202 Acceptedは、受理と完了を区別するHTTP実装として一般的な返し方です。いずれにせよポーリング先が独立したURIになるため、クライアントは接続を維持し続ける必要がなく、進捗確認のリクエストは冪等なGETになります。
コレクション取得の設計
カーソル方式を既定とするページネーション設計
一覧取得の設計で最初に決めるのがページネーション方式です。offsetとlimitで切る方式は実装が簡単ですが、2つの問題を抱えます。1つは性能で、PostgreSQLの公式ドキュメントは「OFFSET句でスキップされる行もサーバ内部で計算する必要があるため、大きなOFFSETは非効率になりうる」と明記しています。10万件目から取得するには、10万行を読み飛ばす作業が毎回発生します。
もう1つがドリフトです。ページをめくっている間に先頭側へ行が挿入されると、以降の行の位置が1つずつずれ、すでに見た行が次のページにもう一度現れます。これはOFFSETが「値」ではなく「行数」で位置を決める方式である以上、避けられません。加えてPostgreSQLの同ドキュメントは、ORDER BYで一意な順序を強制しない限りLIMIT/OFFSETの結果自体が不定になるとも警告しています。
カーソル(キーセット)方式は、行数でスキップせず「最後に見た値」を条件に渡します。WHERE id < :last_seen_id ORDER BY id DESC LIMIT 20 のように書けば、インデックスで直接その位置に到達でき、件数が増えても速度が落ちず、ドリフトも起きません。
実装で落とし穴になるのは、ソートキーが一意でない場合です。created_at順のように同値が並びうる列を単独のカーソルにすると、同一時刻の行を取りこぼしたり重複させたりします。一意な列を組み合わせたタイブレークが必要です。
-- created_at 順(同値がありうる)でのカーソル
SELECT * FROM orders
WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;
API表現としては、AIP-158のpage_size / page_token / next_page_tokenが参考になります。設計上の要点は、ページトークンをクライアントがパースできない不透明な値にすることと、空のnext_page_tokenだけを終端の合図にすることです。トークンの中身がオフセット値だと分かる形式にすると、クライアントがそれを自前で組み立てはじめ、内部方式を変えられなくなります。
フィルタ式の設計:単一パラメータへの集約
絞り込み条件をフィールドごとのクエリパラメータ(?status=paid&min_price=1000)で増やしていくと、条件が増えるたびに仕様が膨らみ、OR条件や否定を表現できなくなります。AIP-160は単一のfilter文字列に式を渡す方式を採り、AND/OR/NOT・比較演算子・フィールド走査をひとつの文法で扱います(不正な式はINVALID_ARGUMENTで拒否)。
小規模なAPIならフィールドごとのパラメータで十分です。分かれ目はOR条件や否定の要望が出てきたかどうか。出てきた時点でパラメータ方式は破綻に向かうため、そこがフィルタ式へ切り替える境目です。
エラー応答とリトライの設計
Problem Details(RFC 9457)によるエラー形式の統一
エラー応答の形式を独自に決めると、クライアントごとにパース処理が分かれます。RFC 9457(2023年7月発行、RFC 7807を廃止)がHTTP APIのエラー応答形式を標準化しており、メディアタイプapplication/problem+jsonにtype(問題種別を識別するURI)、title(人間可読の要約)、status(HTTPステータスと同じ値)、detail(この発生に固有の説明)、instanceを載せます。クライアントはtitleの文字列ではなくtypeのURIで分岐する、というのが仕様の求める使い方です。詳しくはRFC 9457とは?Problem Details(application/problem+json)でHTTP APIエラー応答を標準化【RFC 7807との違い】で解説しています。
レート制限に返す429はRFC 9110ではなくRFC 6585で定義されたステータスコードで、Retry-After(RFC 9110 §10.2.3、HTTP日付または秒数)を添えて再試行時期を伝えられます。クライアント側の再試行は指数バックオフにジッタを加えるのが定石です。AWSの公式解説(2015年3月4日)はsleep = random(0, min(cap, base * 2 ** attempt))というFull Jitterを検証し、総リクエスト数(クライアントの作業量)が最も少なくなる一方、完了までの時間はDecorrelated Jitterよりやや長くなると報告しています。両者の優劣は用途によりますが、ジッタなしの指数バックオフが劣ることは明確です。障害復旧の瞬間に全クライアントの再試行が同じ時刻に揃い、復旧したサーバを再び潰す波を作ります。
バージョニングとスキーマの管理
破壊的変更の線引きとバージョンの上げどき
URIにバージョンを含める方式(/v1/orders)は、キャッシュやログでバージョンが可視になるため運用しやすく、実務での採用が多い方式です。ただし重要なのは方式選びよりも、何を破壊的変更と見なすかを先に決めることです。
ここはGoogleのAIP-180(Backwards compatibility)が明確な基準を出しています。同一メジャーバージョン内で禁止(must not)されるのは、既存フィールド・メソッド・列挙値の削除やリネーム、既存フィールドの型変更(ワイヤ互換であっても不可)、リソース名の変更、既定値の変更です。フィールドの追加は許されますが、既存のリクエストに必須フィールドを追加することは禁止され、追加する任意フィールドは「そのフィールドが無かった頃と同じ挙動」を既定値として持たなければなりません。
この線引きを最初に共有しておけば、多くの変更はv1のまま追加で吸収できます。バージョンを上げるほど並行運用するコードとテストが増えるため、上げずに済ませる基準を持つことがそのまま運用コストの削減になります。
OpenAPIによるリソース定義の機械可読化
リソース定義とメソッドの対応は、OpenAPIで機械可読な形にしておくとクライアント生成・モックが自動化できます。最新版はOpenAPI 3.2.0(2025年9月19日公開)で、「最新は3.1系」という記述はすでに古くなっています。書き方はOpenAPIとは|Swaggerとの違い・仕様書(YAML/JSON)の書き方とSwagger UIをわかりやすく解説を参照してください。
リソース指向で設計すべきでないAPI
ROAは万能ではありません。次の条件に当てはまるなら、リソース指向を無理に貫くほうがコストになります。
操作が本質的に手続きで、名詞に還元すると意味を失う場合。「与信を計算する」「シミュレーションを実行する」のような処理は、結果をリソース化できても、リソース一覧やDELETEに意味がありません。カスタムメソッドが標準メソッドより多いAPIになったら、それはROAが合っていない兆候です。RPC的な方式に寄せたほうが素直で、その比較はtRPCとREST・GraphQLとの違いを徹底比較して理解を深めるが参考になります。
クライアントが画面ごとに異なる形のデータを必要とする場合。リソース単位で正規化されたAPIは、1画面を描くのに複数回の往復が必要になります(N+1リクエスト)。画面数が多くリソース間の関係が深いプロダクトでは、必要な形をクライアントが宣言できる方式が有利です。判断材料はREST APIとGraphQLの違いと使い分け|選定基準と運用コストにまとめています。
社内の1クライアントしか呼ばない内部API。アドレス可能性も接続性も、第三者が発見して辿ることに価値があるから効きます。呼び出し元が自社の1アプリだけなら、その価値は生まれず、設計規約の遵守コストだけが残ります。
逆に、複数チーム・外部パートナーが長期に使い、クライアントの寿命がサーバより長いAPIでは、ROAの制約が効きます。統一インターフェースは「新しい呼び出し元が仕様書を読まなくても振る舞いを予測できる」という形で回収されるため、利用者と年数が増えるほど利く投資です。
よくある質問
リソース指向アーキテクチャとRESTは同じものですか?
同じではありません。RESTはFieldingが2000年に定義したアーキテクチャスタイル(6つの制約)で、ROAはそのRESTをWeb APIとして実装するための設計指針として『RESTful Web Services』(2007)が示したものです。RESTが「守るべき制約」、ROAが「どう設計すれば守れるか」という関係にあります。
PUTとPOSTとPATCHの違いは何ですか?
POSTはコレクションに新しいリソースを作る操作で、冪等ではありません(同じリクエストを2回送ると2件できます)。PUTは指定URIのリソースを全置換する操作で冪等です。PATCHは部分更新で、RFC 5789の定義上は安全でも冪等でもありません。GoogleのAIP-134は更新にPATCHを推奨し、送らなかったフィールドが消えるPUTでの全置換を強く非推奨としています。
URIに動詞を入れてはいけないのはなぜですか?
動詞をHTTPメソッド側に集約するのが統一インターフェースの前提だからです。URIに動詞を入れると、同じ操作が/createOrderと/orders/newのように書き手ごとに増殖し、キャッシュや冪等性の判断もHTTPの意味論から外れます。どうしても手続き的な操作が必要なら、AIP-136のカスタムメソッド(/v1/orders/1001:cancel)としてコロンの右側に隔離します。
APIのページネーションはオフセットとカーソルのどちらを選ぶべきですか?
件数が増える見込みがあるならカーソル(キーセット)方式です。PostgreSQL公式が指摘するとおり、OFFSETはスキップする行もサーバ内部で計算するため大きな値で遅くなり、ページ送り中に行が挿入されると同じ行が重複して現れます。管理画面で「◯ページ目へジャンプ」が要件になっている場合だけ、オフセット方式に合理性があります。
HATEOAS(接続性)は必ず実装しないといけませんか?
必須ではありません。リチャードソン成熟度モデルのレベル3がHATEOASに当たりますが、Martin Fowler自身がRMMは「RESTのレベル」の定義ではないと断っています。実務ではレベル2(HTTPメソッドとステータスコードの正用)で止め、ページネーションの次ページリンクなど接続性が明確に効く箇所にだけリンクを入れる設計が現実的です。