OpenAPIとJSON-RPCの実装の違い
code:OpenAPI (RESTful).sh
リソース指向 - URLがリソースを表現
GET /api/stores # 店舗一覧取得
POST /api/stores # 店舗作成
GET /api/stores/123 # 店舗詳細取得
PUT /api/stores/123 # 店舗更新
DELETE /api/stores/123 # 店舗削除
code:JSON-RPC.sh
メソッド指向 - 単一URLにメソッド名を送信
POST /rpc { "method": "listStore" }
POST /rpc { "method": "addStore" }
POST /rpc { "method": "getStore" }
POST /rpc { "method": "updateStore" }
POST /rpc { "method": "deleteStore" }
2. 実装の具体的な違い
code:go
JSON-RPC実装
サーバー実装 (Go)
package main
import (
"encoding/json"
"net/http"
)
// JSON-RPCは単一のハンドラーですべて処理
func main() {
http.HandleFunc("/rpc", handleRPC)
http.ListenAndServe(":8080", nil)
}
// すべてのリクエストを1つの関数で処理
func handleRPC(w http.ResponseWriter, r *http.Request) {
var req JSONRPCRequest
json.NewDecoder(r.Body).Decode(&req)
var result interface{}
var err error
// メソッド名で分岐
switch req.Method {
case "listStore":
result, err = listStore(req.Params)
case "addStore":
result, err = addStore(req.Params)
case "getStore":
result, err = getStore(req.Params)
case "updateStore":
result, err = updateStore(req.Params)
case "deleteStore":
result, err = deleteStore(req.Params)
default:
err = errors.New("method not found")
}
// 統一されたレスポンス形式
response := JSONRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
}
if err != nil {
response.Error = &JSONRPCError{
Code: -32603,
Message: err.Error(),
}
} else {
response.Result = result
}
json.NewEncoder(w).Encode(response)
}
// パラメータはすべてJSONで受け取る
func listStore(params json.RawMessage) (interface{}, error) {
var input struct {
Paging struct {
Page int json:"page"
Limit int json:"limit"
} json:"paging"
}
json.Unmarshal(params, &input)
// ビジネスロジック
"items": []Store{{ID: 1, Name: "Store A"}},
"paging": input.Paging,
}, nil
}
code:ts
クライアント実装 (TypeScript)
// JSON-RPC Client
export class RPCClient {
private idCounter: number = 1;
// すべてのメソッドが同じ方式で呼び出し
async call(method: string, params: any): Promise<any> {
const response = await fetch(this.endpoint, {
method: 'POST', // 常にPOST
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: method, // メソッド名を指定
params: params,
id: this.idCounter++,
}),
});
const data = await response.json();
if (data.error) {
throw new Error(data.error.message);
}
return data.result;
}
}
// 使用例
const client = new RPCClient();
// すべて同じ方式で呼び出し
await client.call('listStore', { paging: { page: 1, limit: 10 } });
await client.call('addStore', { name: 'New Store', code: 'NST' });
await client.call('getStore', { id: 123 });
await client.call('updateStore', { id: 123, name: 'Updated' });
await client.call('deleteStore', { id: 123 });
主な違いのまとめ
code:sh
| 項目 | OpenAPI (REST) | JSON-RPC |
|----------|------------------------------|-------------|
| URL設計 | /api/stores, /api/stores/123 | /rpc のみ |
| HTTPメソッド | GET, POST, PUT, DELETE | POSTのみ |
| パラメータ送信 | URLパス、クエリ、ボディ | すべてボディ内 |
| エラー処理 | HTTPステータスコード | error フィールド |
| ドキュメント | OpenAPI仕様で自動生成 | 独自定義が必要 |
| キャッシュ | GETは自動キャッシュ可能 | キャッシュ困難 |
| ツール | Swagger UI, Postman対応 | 専用ツールが少ない |
OpenAPIを選ぶべき場合:
- 公開API
- キャッシュが重要
JSON-RPCを選ぶべき場合:
- 内部API
code:js
RESTful(リソース指向)の場合
「リソース」を中心に考える必要があり、時に不自然になることがあります。
// RESTful - 動作をリソースとして表現する必要がある
POST /api/users/123/password-reset // パスワードリセット
POST /api/orders/456/cancel // 注文キャンセル
POST /api/reports/generate // レポート生成
POST /api/emails/send // メール送信
POST /api/calculations/tax // 税金計算
JSON-RPC(メソッド指向)の場合
プログラムの関数呼び出しのように自然に書けます。
// JSON-RPC - 関数を呼ぶように書ける
rpc.call("resetPassword", { userId: 123 })
rpc.call("cancelOrder", { orderId: 456 })
rpc.call("generateReport", { type: "monthly" })
rpc.call("sendEmail", { to: "user@example.com", subject: "Hello" })
rpc.call("calculateTax", { amount: 1000 })
code:js
例:在庫移動処理
RESTfulだと設計に迷う:
// どのリソースにすべきか迷う
POST /api/inventory/transfer ?
POST /api/warehouses/123/transfer-to/456 ?
POST /api/stock-movements ?
JSON-RPCなら明確:
rpc.call("transferInventory", {
fromWarehouseId: 123,
toWarehouseId: 456,
productId: 789,
quantity: 100
})
code:js
1. 初期画面表示で複数データが必要
RESTfulの場合(複数リクエスト)
// 6回の通信が発生
const user = await fetch('/api/users/me')
const notifications = await fetch('/api/notifications')
const orders = await fetch('/api/orders?status=pending')
const products = await fetch('/api/products/recommended')
const cart = await fetch('/api/cart')
const settings = await fetch('/api/settings')
JSON-RPCのバッチ処理(1回の通信)
// 1回の通信ですべて取得
const response = await fetch('/rpc', {
method: 'POST',
body: JSON.stringify([
{ jsonrpc: "2.0", method: "getCurrentUser", id: 1 },
{ jsonrpc: "2.0", method: "getNotifications", id: 2 },
{ jsonrpc: "2.0", method: "getPendingOrders", id: 3 },
{ jsonrpc: "2.0", method: "getRecommendedProducts", id: 4 },
{ jsonrpc: "2.0", method: "getCart", id: 5 },
{ jsonrpc: "2.0", method: "getSettings", id: 6 }
])
})
// レスポンスも配列で返る
[
{ jsonrpc: "2.0", id: 1, result: { user: {...} } },
{ jsonrpc: "2.0", id: 2, result: { notifications: ... } }, { jsonrpc: "2.0", id: 3, result: { orders: ... } }, // ...
]
code:js
2. 大量データの一括処理
例:100件の商品価格を一括更新
RESTfulの場合:
// 100回のリクエストが必要
for (const product of products) {
await fetch(/api/products/${product.id}, {
method: 'PUT',
body: JSON.stringify({ price: product.newPrice })
})
}
JSON-RPCバッチ:
// 1回のリクエストで100件処理
const batch = products.map((product, index) => ({
jsonrpc: "2.0",
method: "updateProductPrice",
params: { id: product.id, price: product.newPrice },
id: index
}))
await fetch('/rpc', {
method: 'POST',
body: JSON.stringify(batch)
})
code:js
3. トランザクション的な処理
例:ECサイトの注文確定処理
// JSON-RPCバッチで原子的に処理
const response = await fetch('/rpc', {
method: 'POST',
body: JSON.stringify([
{ method: "validateCart", params: { cartId: 123 }, id: 1 },
{ method: "checkInventory", params: { items: ... }, id: 2 }, { method: "calculateShipping", params: { address: {...} }, id: 3 },
{ method: "applyDiscount", params: { code: "SAVE10" }, id: 4 },
{ method: "createOrder", params: { ... }, id: 5 },
{ method: "processPayment", params: { ... }, id: 6 },
{ method: "sendConfirmationEmail", params: { ... }, id: 7 }
])
})
// すべて成功したか確認
const hasError = response.some(r => r.error)
if (hasError) {
// ロールバック処理
}
code:js
4. モバイルアプリでの通信最適化
// 通信回数を減らして電池消費とレイテンシーを削減
class MobileAPI {
async syncData() {
// 1回の通信で必要なデータをすべて同期
return rpcBatch([
{ method: "getUpdatedContacts", params: { since: lastSync } },
{ method: "getNewMessages", params: { since: lastSync } },
{ method: "uploadPendingPhotos", params: { photos: ... } }, { method: "getServerTime", params: {} }
])
}
}