RESTful APIで複合リソースを設計する:複数のデータを組み合わせて表現する方法
はじめに
RESTful API設計において、リソースは通常、システム内の個々のエンティティや概念(例: ユーザー、商品、注文など)に対応させて設計されます。これは、リソースに対するCRUD操作(作成、読み取り、更新、削除)を直感的に定義できるという点で、非常に強力なアプローチです。
しかし、実際のアプリケーション開発では、画面表示やレポート作成のために、複数のリソースに含まれる情報を組み合わせて一度に取得したいというニーズが頻繁に発生します。例えば、注文一覧画面で、各注文情報に紐づく顧客名や商品名を合わせて表示したい、といったケースです。
このような場合、単純に個々のリソースAPI(例: /orders
, /customers
, /products
)をクライアント側で何度も呼び出し、それらを組み合わせてデータを整形するという方法も考えられます。しかし、APIコールの回数が増えたり、クライアント側のデータ結合ロジックが複雑になったりするなど、様々な非効率や課題が生じることがあります。
そこで重要になるのが、複数の基本リソースに含まれる情報を集約したり組み合わせたりして提供する「複合リソース」の設計です。この記事では、RESTful APIにおける複合リソースの考え方と、いくつかの設計パターン、そして設計時に考慮すべき点について解説します。
なぜ複合リソースが必要か? 課題の提起
基本的なリソースAPIだけでは、以下のような課題に直面することがあります。
- パフォーマンスの低下: 必要なデータを取得するためにN+1問題(一覧表示のために親リソースをN個取得し、それぞれの子リソースを取得するためにさらにN回APIコールするなど)が発生し、レイテンシが増大します。
- クライアント側の複雑化: 複数のAPIからのレスポンスを受け取り、クライアント側でデータを結合・整形するロジックが必要になります。これはクライアントアプリケーションの実装を複雑にし、保守性を低下させる要因となります。
- データの一貫性の問題: 複数のAPIコールを連続して行う間に、データが更新される可能性があり、取得したデータ間に一時的な不整合が生じるリスクがあります。
これらの課題を解決し、クライアントからの利用効率を高めるために、サーバー側でデータを集約・整形して提供する複合リソースの設計が有効になります。しかし、「どのような粒度で」「どのような構造で」複合リソースを設計すべきか、判断に迷うことも少なくありません。
複合リソースの基本的な考え方
複合リソースは、既存の個別の「基本リソース」を組み合わせたり、そこから派生・集約された情報を提供するリソースと考えることができます。その主な目的は、特定のユースケース(例えば、特定の画面表示やレポート出力)に必要なデータを、最小限のAPIコールで、クライアントが扱いやすい構造で提供することにあります。
複合リソースは必ずしもCRUDの全てをサポートする必要はありません。多くの場合、データの「読み取り(GET)」に特化したビューのような役割を果たします。データ更新が必要な場合は、その元となる個別の基本リソースに対するPUTやPATCHメソッドを使用することが一般的です。
複合リソースの設計にあたっては、以下の点を意識することが重要です。
- ユースケース駆動: どのようなクライアントが、どのような目的でそのデータを必要とするのかを明確にします。
- 情報粒度: クライアントが必要とする最小限かつ十分な情報を含めるようにします。
- 保守性: 複合リソースの設計が、元となる基本リソースの変更にどの程度影響されるか、その影響をどう管理するかを考慮します。
複合リソースの設計パターン
複合リソースをAPIとして表現する方法にはいくつかのパターンがあります。代表的なものを紹介します。
パターン1: 新しいトップレベルリソースとして定義する
特定のユースケースのために、複数の基本リソースから必要な情報を集約し、新しい独立したリソースとして定義するパターンです。
例: 注文とそれに紐づく顧客情報、商品情報の一部を組み合わせた「注文詳細ビュー」のようなリソース。
- エンドポイント例:
GET /orders-with-details/{orderId}
- レスポンス構造例 (JSON):
{
"orderId": "order-123",
"orderDate": "2023-10-27T10:00:00Z",
"totalAmount": 12500,
"customer": { // 顧客情報をネスト
"customerId": "cust-456",
"customerName": "山田太郎"
},
"items": [ // 注文アイテム情報をネスト
{
"itemId": "item-a",
"productId": "prod-xyz",
"productName": "商品A",
"quantity": 1,
"unitPrice": 5000
},
{
"itemId": "item-b",
"productId": "prod-uvw",
"productName": "商品B",
"quantity": 3,
"unitPrice": 2500
}
]
}
この例では、注文(Order)、顧客(Customer)、商品(Product)、注文アイテム(OrderItem)といった基本リソースの情報が組み合わされています。顧客名や商品名は、元の基本リソースから取得された情報です。
- メリット:
- 特定のユースケースに最適化されており、クライアントは必要なデータを1回のAPIコールで取得できます。
- エンドポイント名で用途が明確になります。
- デメリット:
- ユースケースごとに類似したリソースが増える可能性があります。
- 汎用性が低く、構造が固定されるため、少し異なる情報が必要な場合に対応しづらいことがあります。
- 原則として読み取り専用とすることが多いため、更新系の操作は提供しません。
パターン2: 既存リソースの拡張表現として定義する (Embedding/Expanding)
基本的なリソースを取得する際、関連する他のリソース情報もレスポンスに含める(埋め込む/展開する)ように指定するパターンです。HATEOASの一部として関連リソースへのリンクを含める方法とも関連しますが、ここでは直接データをペイロードに含めることに焦点を当てます。
クエリパラメータを使用して、どの関連リソースを埋め込むか制御することが一般的です。
例: 注文リストを取得する際に、各注文に紐づく顧客情報とアイテム情報を埋め込む。
- エンドポイント例:
GET /orders?_embed=customer,items
- レスポンス構造例 (JSON):
[
{
"id": "order-123",
"orderDate": "2023-10-27T10:00:00Z",
"totalAmount": 12500,
"customerId": "cust-456",
"_embedded": { // 関連リソースを"_embedded"などのキーの下にまとめることが多い
"customer": {
"id": "cust-456",
"name": "山田太郎"
},
"items": [
{
"id": "item-a",
"orderId": "order-123",
"productId": "prod-xyz",
"quantity": 1,
"unitPrice": 5000,
"_embedded": { // アイテムに関連する商品情報をさらに埋め込むことも可能
"product": {
"id": "prod-xyz",
"name": "商品A"
}
}
},
// ... 他のアイテム
]
}
},
// ... 他の注文
]
この例では、_embed
クエリパラメータで指定されたcustomer
とitems
が、元の注文リソースの構造内に_embedded
というキーを使って含まれています。
- メリット:
- 基本リソースのエンドポイントを維持しつつ、柔軟に関連データを取得できます。
- クライアントが必要な関連データだけを選択して取得できるため、不要なデータ転送を削減できます。
- デメリット:
- クエリパラメータの命名規約を明確にする必要があります(例:
_embed
,_expand
,include
など)。 - 埋め込む関連データの数が多くなると、レスポンスの構造が深くなり、クライアント側での処理が複雑になる可能性があります。
- レスポンスサイズが大きくなりやすい場合は注意が必要です。
- クエリパラメータの命名規約を明確にする必要があります(例:
パターン3: ネストしたリソースとして定義する
親リソースの下に、特定の条件で集約またはフィルタリングされた子リソースや関連リソースのサマリーを表現するパターンです。
例: 特定の顧客の最新の注文サマリーリスト。
- エンドポイント例:
GET /customers/{customerId}/order-summaries
- レスポンス構造例 (JSON):
[
{
"orderId": "order-123",
"orderDate": "2023-10-27T10:00:00Z",
"totalAmount": 12500,
"numberOfItems": 2 // 集約された情報
},
{
"orderId": "order-456",
"orderDate": "2023-10-20T09:15:00Z",
"totalAmount": 8000,
"numberOfItems": 1
}
]
この例では、/customers/{customerId}
という親リソースの下に/order-summaries
というサブリソースを定義し、顧客ごとの注文サマリーを提供しています。サマリーなので、すべての注文アイテム情報は含まれていません。
- メリット:
- リソース間の階層関係をURLで表現できます。
- 特定の親リソースに関連する集約データやフィルタリングされたデータを取得するのに適しています。
- デメリット:
- リソースパスが深くなりすぎる可能性があります。
- 階層のトップからでないとアクセスしにくい構造になります。
データモデリングにおける考慮事項
複合リソースを設計する際には、いくつかの重要なデータモデリングの考慮事項があります。
-
必要な情報の粒度:
- クライアントがその複合リソースを何に使うのかを具体的に考え、本当に必要な情報だけを含めます。
- 過剰に情報を含めると、レスポンスサイズが増大し、セキュリティリスク(不要なデータの露出)やパフォーマンス劣化につながります。
- 逆に情報が不足していると、結局クライアントが別のAPIを呼び出すことになり、複合リソースのメリットが薄れます。
- 例えば、注文詳細ビューで、顧客の住所や電話番号が不要であれば含めない、といった判断が必要です。
-
フラット化 vs ネスト化:
- レスポンスのJSON構造を、関連データをネストさせるか、一部の情報をトップレベルにフラットに配置するか検討します。
- ネスト化は元のリソース構造を反映しやすく、関連性が分かりやすいですが、構造が深くなりがちです。
- フラット化はシンプルな構造になりますが、どの情報がどの元のリソース由来なのか分かりにくくなることがあります。
- ユースケースに合わせて、どちらがクライアントにとって扱いやすい構造かを判断します。例えば、リスト表示で一覧性を重視するならフラット化が、詳細表示でデータ構造の関連性を重視するならネスト化が適している場合があります。
-
識別子 (ID) の扱い:
- 複合リソースに、含まれる各エンティティ(元の基本リソース)のIDを含めるべきか検討します。
- IDを含めることで、クライアントはその複合リソース内のデータを使って、個別の基本リソースAPIを呼び出すことが容易になります(例: 注文詳細から顧客IDを取得して、顧客詳細画面へ遷移するなど)。
- 更新操作が必要な場合は、どのエンティティのどの情報を更新したいのかを特定するためにIDが不可欠です。しかし、複合リソースは基本的に読み取り専用ビューとすることが推奨されます。
-
データの鮮度:
- 複合リソースに含まれる複数のデータの鮮度について考慮が必要です。例えば、注文と在庫情報を組み合わせたリソースを提供する際、データの取得タイミングによって在庫数が変動している可能性があります。
- リアルタイム性が求められる場合は、データの取得方法やキャッシュ戦略に注意が必要です。
-
更新操作への影響:
- 繰り返しになりますが、複合リソースは原則として読み取り専用ビューとして設計することが推奨されます。
- 複合リソースの一部(例: 注文詳細ビューに含まれる顧客名)を更新しようとすると、その更新が元の基本リソース(顧客リソース)にどのように反映されるべきか、ビジネスロジックが複雑になりがちです。
- 更新操作が必要な場合は、対応する個別の基本リソースAPI (
PUT /customers/{customerId}
) を使用するようにクライアントに促すべきです。
アンチパターン
複合リソース設計における一般的なアンチパターンです。
- 「全部入り」レスポンス: 特定のユースケースに必要ない情報まで含めて、レスポンスが肥大化してしまうパターンです。これはパフォーマンスの悪化を招き、クライアント側のメモリ消費も増やします。
- 一貫性のない構造: 類似の目的を持つ複合リソースなのに、構造や命名規則がバラバラなパターンです。APIを利用するクライアント側での学習コストが増え、開発効率が低下します。API設計全体での一貫性を保つことが重要です。
- 複合リソースでの更新操作提供: 複合リソース全体、あるいはその一部に対するPUT/PATCH/DELETE操作を提供しようとするパターンです。これはサーバー側のビジネスロジックを複雑にし、予期せぬ副作用を引き起こす可能性があります。例えば、注文詳細ビューの一部を更新しようとした際に、それが注文自体、顧客、商品、あるいは注文アイテムのどれに影響するのか、曖昧になりがちです。
実践的なヒント
- まずは基本リソースから: 最初から全ての複合リソースを設計しようとせず、まずは堅牢な基本リソースAPIを設計します。その上で、クライアントからの具体的な要求に応じて複合リソースを検討するのが良いアプローチです。
- ユースケースを明確に: どのような画面や機能のためにこの複合リソースが必要なのか、そのユースケースを明確に定義し、それに最適化された設計を行います。
- ドキュメント化を徹底: 複合リソースの構造、含まれる各フィールドがどの基本リソース由来の情報なのか、読み取り専用であることなどをAPIドキュメントに明確に記述します。
- バージョン管理への配慮: 複合リソースは複数の基本リソースに依存するため、どれか一つの基本リソースの変更が複合リソースに影響を及ぼす可能性があります。後方互換性を保ちつつ進化させるためのバージョン管理戦略がより重要になります。
まとめ
RESTful APIにおける複合リソースの設計は、クライアント側の効率性とシンプルさを向上させる上で非常に有効な手法です。しかし、その設計は単なる基本リソースの組み合わせではなく、特定のユースケースやデータ利用の目的に深く関連しています。
この記事で紹介したように、新しいトップレベルリソース、既存リソースの拡張表現、ネストしたリソースといったパターンや、情報の粒度、構造、更新操作といった考慮事項を踏まえ、複合リソースが本当に必要かを見極め、必要であれば利用シーンに最適な設計を選択することが重要です。
複合リソースを上手に活用することで、APIの利用者であるクライアント開発者にとって、より使いやすく効率的なAPIを提供できるでしょう。まずは小さな複合リソースから設計を始め、経験を積んでいくことをお勧めします。