LESSON 40分

ストーリー

佐藤CTO
サービスを分けたら、次は”どう話すか”を決めなければならない
佐藤CTO
API契約はサービス間の約束事だ。この設計を間違えると、変更のたびに全チームが巻き込まれる

通信プロトコルの選択

REST vs gRPC vs GraphQL

項目RESTgRPCGraphQL
プロトコルHTTP/1.1HTTP/2HTTP/1.1
データ形式JSONProtocol BuffersJSON
型安全性△(OpenAPI)◎(Proto定義)○(Schema)
パフォーマンス
ストリーミング△(SSE)◎(双方向)△(Subscription)
ブラウザ対応△(gRPC-Web)
学習コスト
主な用途外部APIサービス間通信BFF/フロント向け

使い分けの指針

外部公開API → REST(広い互換性)
サービス間同期通信 → gRPC(型安全・高速)
フロントエンド向け → GraphQL(柔軟なクエリ)
サービス間非同期 → イベント(Kafka/SQS)

API契約の設計原則

Consumer-Driven Contracts

消費者(呼び出し側)が必要な契約を定義し、提供者がそれを満たす:

// Consumer(注文サービス)が定義する契約
// "商品サービスにはこのレスポンスを期待する"
interface ProductContract {
  id: string;
  name: string;
  price: number;
  inStock: boolean;
  // 注文サービスはこの4フィールドだけ必要
  // 商品の画像URLやカテゴリは不要
}

// Pact によるコントラクトテスト
describe('Product Service Contract', () => {
  it('returns product details', async () => {
    // Consumer が期待するレスポンスを定義
    await provider.addInteraction({
      state: 'product ABC exists',
      uponReceiving: 'a request for product ABC',
      withRequest: {
        method: 'GET',
        path: '/products/ABC',
      },
      willRespondWith: {
        status: 200,
        body: {
          id: like('ABC'),
          name: like('商品名'),
          price: like(1000),
          inStock: like(true),
        },
      },
    });
  });
});

APIバージョニング戦略

戦略方法メリットデメリット
URLパス/v1/products明確、キャッシュしやすいURL変更が必要
ヘッダーAccept: application/vnd.api.v2+jsonURLが変わらない発見しにくい
クエリパラメータ?version=2シンプルキャッシュキー複雑

後方互換性の維持

// ✅ 後方互換: フィールド追加(既存クライアントは無視可能)
// v1: { id, name, price }
// v2: { id, name, price, description, category }

// ❌ 破壊的変更: フィールド名変更
// v1: { product_name }
// v2: { name }  ← 既存クライアントが壊れる

// ✅ 推奨: Tolerant Reader パターン
class ProductClient {
  parseResponse(data: unknown): Product {
    // 未知のフィールドは無視する
    const parsed = productSchema.safeParse(data);
    if (!parsed.success) {
      // フォールバック処理
      return this.parseV1Response(data);
    }
    return parsed.data;
  }
}

gRPC によるサービス間通信

// product.proto
syntax = "proto3";

service ProductService {
  rpc GetProduct(GetProductRequest) returns (Product);
  rpc ListProducts(ListProductsRequest) returns (stream Product);
  rpc UpdateStock(UpdateStockRequest) returns (UpdateStockResponse);
}

message Product {
  string id = 1;
  string name = 2;
  int64 price_cents = 3;
  bool in_stock = 4;
  // 新フィールドは番号を追加するだけ(後方互換)
  string description = 5;
}

message GetProductRequest {
  string product_id = 1;
}
// gRPC クライアント実装
class ProductGrpcClient {
  private client: ProductServiceClient;

  constructor(address: string) {
    this.client = new ProductServiceClient(
      address,
      grpc.credentials.createInsecure()
    );
  }

  async getProduct(productId: string): Promise<Product> {
    return new Promise((resolve, reject) => {
      this.client.getProduct(
        { productId },
        (error, response) => {
          if (error) reject(error);
          else resolve(response);
        }
      );
    });
  }
}

API ゲートウェイパターン

graph TD
    Client["クライアント"] --> GW["API Gateway\n認証/認可\nレート制限\nルーティング\nレスポンス集約\nプロトコル変換"]
    GW --> OrderSvc["注文 svc"]
    GW --> ProductSvc["商品 svc"]
    GW --> CustomerSvc["顧客 svc"]

    style Client fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style GW fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#5b21b6
    style OrderSvc fill:#d1fae5,stroke:#059669,color:#065f46
    style ProductSvc fill:#d1fae5,stroke:#059669,color:#065f46
    style CustomerSvc fill:#d1fae5,stroke:#059669,color:#065f46

BFF(Backend for Frontend)パターン:

Web Client → Web BFF → 各サービス
Mobile App → Mobile BFF → 各サービス
Admin UI  → Admin BFF → 各サービス

まとめ

ポイント内容
プロトコル選択用途に応じてREST/gRPC/GraphQLを使い分け
Consumer-Driven消費者が契約を定義し、コントラクトテスト
バージョニング後方互換性を維持しながら進化
API Gateway横断的関心事を集約し、BFFで最適化

チェックリスト

  • REST/gRPC/GraphQL の使い分けを判断できる
  • Consumer-Driven Contract の概念を理解した
  • API バージョニング戦略を選択できる
  • API Gateway/BFF パターンを説明できる

次のステップへ

次は演習でサービス境界設計に挑戦します。


推定読了時間: 40分