RESTful APIでビジネス操作を表現するデータモデリング:アクションとイベントの設計
はじめに:なぜビジネス操作のモデリングが重要なのか
RESTful APIの基本的な考え方は「リソース指向」です。URIでリソースを特定し、HTTPメソッド(GET, POST, PUT, PATCH, DELETE)でリソースに対する標準的な操作(CRUD: Create, Read, Update, Delete)を行うというシンプルなモデルは、多くの場面で有効です。
しかし、現実のビジネスシステムでは、単なるリソースのCRUD操作だけでは表現しきれない複雑な「操作」や「イベント」が多く存在します。例えば、「注文をキャンセルする」「記事を公開する」「ユーザーを承認する」「メールを送信する」といった操作は、特定のリソース(注文、記事、ユーザーなど)の状態変化を伴いますが、その操作自体が単なるPUTやPATCHでは表現しきれない場合や、複数のリソースに影響を及ぼす場合があります。
こうしたビジネス固有の複雑な操作をAPIとしてどのように設計するかは、多くのエンジニアが直面する課題です。特に、リソース指向の原則を守りつつ、APIの表現力、保守性、使いやすさを両立させるためには、操作そのものに対するデータモデリングの考え方が不可欠になります。
本記事では、RESTful APIにおいて、CRUDの枠を超えたビジネス操作やイベントをどのようにデータモデリングし、APIとして設計するかについて解説します。
CRUDだけでは表現しきれない操作とは?
RESTful APIは、システムをリソースの集合として捉え、各リソースに対して標準的な操作を適用するという強力なパラダイムを提供します。しかし、以下のようなケースでは、単にリソースの状態をPUTやPATCHで更新するだけでは不十分な場合があります。
- 複雑なビジネスロジックを伴う操作: 例:商品の購入処理(在庫引き当て、決済処理、注文履歴作成など複数のステップを含む)
- 不可逆的、あるいは特別な権限が必要な操作: 例:アカウントの永久削除、重要な申請の承認/却下
- 副作用が大きい操作: 例:システム設定の変更、全ユーザーへの通知送信
- 操作自体が冪等性を持ちにくい場合: 例:メール送信、ポイント付与
- 複数のリソースに跨る操作: 例:会議室予約(会議室リソース、参加者リソース、スケジュールリソースなどに関連)
これらの操作は、単にリソースの属性値を変更するのではなく、システム全体の状態に影響を与えたり、特定のビジネスイベントをトリガーしたりする性質を持っています。
操作をRESTful APIでモデリングするアプローチ
これらのCRUDでは表現しにくいビジネス操作を、RESTfulの原則に則ってAPIとして表現するにはいくつかのアプローチがあります。重要なのは、これらの操作をシステム内の「何か」に対する「作用」として捉え、それをAPIリソースやエンドポイント設計に反映させることです。
主なアプローチとしては、以下のパターンが考えられます。
-
操作をリソースのサブリソースとして表現する: 特定の親リソースに対して行われる操作を、そのリソースのサブリソースとして表現するパターンです。操作の種類ごとにURIを設計し、通常はPOSTメソッドを使用します。
- 例:注文キャンセル操作
POST /orders/{orderId}/cancel
- 例:記事公開操作
POST /articles/{articleId}/publish
- 例:ユーザー承認操作
POST /users/{userId}/approve
このアプローチのメリットは、操作の対象となるリソースが明確であること、URIの構造が直感的になりやすいことです。デメリットとしては、操作の種類が増えるとURIが長くなりがちであること、リソースに対する操作というよりは、操作そのものをリソースとして捉える(コントローラーリソースの考え方)に近い側面があることです。
- 例:注文キャンセル操作
-
操作を独立した「コマンド」や「アクション」リソースとして表現する(コントローラーリソース): 特定の種類の操作を集約するコントローラーリソースを設け、そのリソースに対してPOSTメソッドで操作の詳細をリクエストボディとして送信するパターンです。リソース指向の厳密な解釈からは外れるとする向きもありますが、特定のビジネス操作を表現する際には非常に実用的です。
- 例:汎用的な操作エンドポイント
POST /actions
(リクエストボディで操作の種類とパラメータを指定) - 例:特定の操作タイプに特化したエンドポイント
POST /commands/order-cancel
POST /commands/article-publish
このアプローチのメリットは、URIの構造が比較的シンプルになる可能性があること、多様な操作を柔軟に扱えることです。デメリットとしては、URIだけでは操作対象が分かりにくい場合がある(リクエストボディを見る必要がある)、汎用的な
POST /actions
のような設計はAPIの discoverability を損ないやすいこと、APIドキュメントが重要になることです。一般的には、POST /commands/{action-type}
のように、ある程度操作の種類でエンドポイントを分ける方が分かりやすくなります。 - 例:汎用的な操作エンドポイント
-
リソースのPUT/PATCHによる状態変更として表現する: 操作の結果としてリソースの状態が変化する場合、その状態をリソースの属性として持ち、PUTまたはPATCHでその属性を更新することで操作を表現するアプローチです。
- 例:注文ステータス変更
PATCH /orders/{orderId}
(リクエストボディ:{ "status": "cancelled" }
) - 例:記事ステータス変更
PATCH /articles/{articleId}
(リクエストボディ:{ "status": "published" }
)
このアプローチは、操作がリソースの特定の属性への単純な状態遷移である場合に最もRESTfulな方法と言えます。しかし、状態遷移の裏側に複雑なビジネスロジックがある場合(例: "cancelled" にする際に在庫を戻す、返金処理を行うなど)、クライアントはその裏側の副作用を知らずにリクエストすることになり、APIの意図が不明確になる可能性があります。また、同じステータスでも異なるビジネスロジックが実行される場合(例: 管理者によるキャンセルとユーザー自身によるキャンセル)、この方法では区別が難しくなります。
- 例:注文ステータス変更
これらのアプローチの中から、操作の性質、複雑さ、対象リソースとの関連性を考慮して最適な方法を選択することが重要です。多くの場合、操作をサブリソースとして表現する (POST /resource/{id}/operation
) 方法が、リソース指向とのバランスを取りやすく、推奨されることが多いです。
リクエスト・レスポンスのデータ構造設計
操作を表現するAPIエンドポイントでは、リクエストボディで操作に必要な情報を伝え、レスポンスボディで操作結果や関連情報、状態などを返す必要があります。このデータ構造設計が、APIの使いやすさや保守性に大きく影響します。
リクエストボディの設計
操作の種類に応じて、クライアントから送信されるべき情報をリクエストボディに含めます。
-
サブリソースとして表現する場合(例:
POST /orders/{orderId}/cancel
): パスパラメータ{orderId}
で対象リソースを指定しているため、リクエストボディには操作の実行に必要な追加情報を含めます。例えば、キャンセルの理由や、キャンセル時に適用されるポリシーなどが考えられます。json { "cancellation_reason": "Customer changed mind", "apply_restocking_fee": true }
-
コントローラーリソースとして表現する場合(例:
POST /commands/order-cancel
): 対象リソースのIDも含めて、操作に必要な全ての情報をリクエストボディに含めます。json { "order_id": "ord_12345", "cancellation_reason": "Customer changed mind", "apply_restocking_fee": true }
リクエストボディの設計では、以下の点を考慮すると良いでしょう。
- 必要最小限の情報を含める: 操作の実行に本当に必要な情報だけを含めます。過剰な情報を含めると、APIの結合度が高まり、変更に弱くなります。
- 明確な属性名を使用する: 属性名からその情報の意味や用途が分かるようにします。
- データ型と制約を定義する: 各属性のデータ型(文字列、数値、真偽値など)や、必須/任意、値の制約などを明確にします(APIスキーマ定義ツールを使用すると効果的です)。
レスポンスボディの設計
操作を実行した結果をレスポンスボディで返します。これは操作が成功したかどうかの情報、操作によって変更されたリソースの最新状態、あるいは操作の結果生成された新しいリソースなどを含みます。
-
操作が成功した場合 (HTTPステータスコード 2xx): 一般的には、操作の結果として得られるリソースの最新状態、あるいは操作の成功を示すシンプルなレスポンスを返します。
-
状態が変更されたリソースを返す: 操作対象のリソースの最新状態を返すと、クライアントは再取得の手間なく最新情報を得られます。 例:
POST /orders/{orderId}/cancel
成功時のレスポンス (ステータスコード 200 OK)json { "id": "ord_12345", "status": "cancelled", "cancellation_date": "2023-10-27T10:00:00Z", "items": [ ... ], "total_amount": 5000, ... 他の注文情報 }
* 操作結果を示すシンプルなレスポンスを返す: 状態変更されたリソース全体を返すのが適切でない場合や、操作の結果が単一のリソースの状態変化だけではない場合に有効です。操作の成否や、操作によって発生したイベントIDなどを返します。 例:POST /users/{userId}/send-welcome-email
成功時のレスポンス (ステータスコード 200 OK または 202 Accepted)json { "success": true, "message": "Welcome email sent successfully.", "task_id": "tsk_abcde" // 非同期処理の場合など }
* 新しいリソースが作成された場合: 操作の結果、新しいリソースが作成される場合は、201 Created
ステータスコードとともに、作成されたリソースの情報を返します。Location
ヘッダーに作成されたリソースのURIを含めるのがRESTfulな作法です。 例:POST /process-order
-> 注文リソース作成 (ステータスコード 201 Created)json { "id": "ord_12345", "status": "pending", ... 他の注文情報 }
Location:/orders/ord_12345
-
-
操作が失敗した場合 (HTTPステータスコード 4xx/5xx): 操作の失敗原因をクライアントに分かりやすく伝えるためのデータ構造設計が重要です。既存記事「API設計におけるエラーレスポンスのデータ構造」も参照してください。一般的には、エラーコード、エラーメッセージ、可能であれば具体的なエラー箇所(どのパラメータが不正だったかなど)を含めます。
例: リクエストボディのパラメータが不正だった場合 (ステータスコード 400 Bad Request)
json { "code": "invalid_parameter", "message": "The provided cancellation reason is too short.", "details": { "parameter": "cancellation_reason", "minimum_length": 10 } }
レスポンスボディの設計では、以下の点を考慮すると良いでしょう。
- 操作の結果と状態を正確に伝える: クライアントが操作の結果を正しく理解し、次のアクションを取れるように、必要な情報を過不足なく含めます。
- 関連リソースの取得: 操作の結果として関連リソースの情報が必要になる場合、そのリソースへのURIを含める(HATEOASの考え方)か、必要に応じてレスポンスに含める(データ詳細度制御の検討)かを判断します。ただし、操作結果とは直接関係ない大量の関連データを無条件に返すのは避けるべきです。
- 一貫性のあるエラー表現: どのような操作であっても、エラー時のレスポンス構造は統一することで、クライアントの実装を容易にします。
操作モデリングにおける考慮事項
ビジネス操作をAPIとしてモデリングする際には、データ構造だけでなく、API全体の設計思想や特性も考慮する必要があります。
- 冪等性 (Idempotency): 同じリクエストを複数回実行しても、一度実行した場合と同じ結果になる性質です。GET, PUT, DELETEは通常冪等ですが、POSTは冪等ではありません。操作を表現するAPIが冪等であるべきか(例: 記事を公開する操作は何回行っても「公開済み」という結果は同じ)、そうでないか(例: ポイントを付与する操作は回数分だけ付与される)を明確にし、必要に応じてクライアントからの冪等キーの受け付けや、サーバーサイドでの冪等性担保の仕組みを検討します。
- 非同期処理: 複雑なビジネス操作は、即座に完了しない場合があります。その場合は、操作開始のエンドポイントを同期的に呼び出し、処理を受け付けたことを示す
202 Accepted
を返し、実際の処理は非同期で行う設計が考えられます。この際、処理の状態を確認するためのポーリング用エンドポイントや、処理完了を通知するWebhookなどの仕組みと合わせて、処理の状態(processing
,completed
,failed
など)をどのようにデータモデリングし、クライアントに通知するかを設計する必要があります。(既存記事「RESTful APIで非同期処理を扱うデータモデリング」も参考にしてください。) - トランザクション: 複数のシステムやデータに跨る操作は、アトミックなトランザクションとして実行される必要があります。APIの設計はこのトランザクションの境界を反映するべきです。失敗時には適切にロールバックされるか、補償トランザクションが可能な設計になっているかを確認します。
- ドキュメント化: ビジネス操作を表現するAPIは、その操作の目的、必要な入力、期待される出力、発生しうるエラーなどを明確にドキュメント化することが特に重要です。APIスキーマ定義ツール(OpenAPIなど)を活用し、リクエストボディ、レスポンスボディ、エラーレスポンスのデータ構造を詳細に定義します。
アンチパターン
ビジネス操作のモデリングで陥りがちなアンチパターンを避けることも重要です。
- なんでもかんでも POST /actions に集約する: 操作の種類が増えるたびにリクエストボディの構造が複雑になり、APIの利用者はドキュメントを読まないとどのような操作が可能か、どのようなパラメータが必要か全く分かりません。APIの discoverability を著しく損ないます。
- 操作に必要なデータをURIクエリパラメータに含めすぎる: URIが非常に長くなるだけでなく、機密情報を含む可能性があるデータをURIに含めてしまうリスクがあります。リクエストボディを使用する方が適切です。
- 操作成功レスポンスで無関係な大量データを返す: 例: 注文キャンセルAPIの成功レスポンスで、キャンセルされた注文に関連する顧客の過去の全注文履歴を返すなど。APIのレスポンスは操作の結果に直接関連する情報に絞るべきです。
- 操作の実行結果とデータ取得をごちゃ混ぜにする: 例:
GET /reports/generate?type=sales&year=2023
のような設計で、レポート生成という操作を実行しつつ、その結果を返す。GETメソッドは冪等かつ安全であるべきであり、状態変更を伴う操作にはPOSTなどの他のメソッドを使用すべきです。
まとめ
RESTful APIにおいて、CRUD操作では表現しきれない複雑なビジネス操作やイベントをデータモデリングすることは、APIの表現力と実用性を高める上で不可欠です。
- 操作をどのようにAPIエンドポイントとして表現するか(サブリソース、コントローラーリソースなど)を検討し、操作の性質に合ったアプローチを選択します。多くの場合、
POST /resource/{id}/operation
の形式がリソース指向とのバランスが良いです。 - リクエストボディ、レスポンスボディのデータ構造は、操作に必要な入力と、操作の結果・状態を正確に伝えるように設計します。具体的で分かりやすい属性名を使い、スキーマ定義を明確に行います。
- 冪等性、非同期処理、トランザクションといったAPIの特性を考慮し、データモデリングと連携させて設計を進めます。
- アンチパターンに注意し、URIの適切性、リクエストボディの使用、レスポンスの関連性などを常に意識します。
これらの考え方を取り入れることで、単なるデータストアへのインターフェースではなく、ビジネスロジックを適切にカプセル化し、クライアントにとって使いやすく、サーバーサイドで保守しやすいAPIを実現できるでしょう。API設計は常に進化するものであり、完璧な設計は難しいかもしれませんが、これらの原則を理解し、チーム内で議論を重ねながら、より良いデータモデリングを目指していくことが大切です。