TL;DR (要約)
プロトコル層からビジネスロジックを分離することで、マルチプロトコルAPIを構築します。共通のドメイン層を作成し、その上にREST、GraphQL、gRPCアダプターを追加します。モダンPetstoreAPIは、これら3つのプロトコルすべてで一貫したデータモデルを持つこのアーキテクチャを示しています。
はじめに
あなたのAPIは、ウェブクライアント、モバイルアプリ、内部マイクロサービスにサービスを提供しています。ウェブクライアントはシンプルさのためにRESTを、モバイルアプリはデータ転送を減らすためにGraphQLを、マイクロサービスはパフォーマンスのためにgRPCを求めています。これら3つのために、それぞれ別々のAPIを構築しますか?
いいえ。3つのプロトコル層を持つ1つのAPIを構築します。ビジネスロジックは同じままで、プロトコルアダプターだけが変更されます。これがマルチプロトコルAPIアーキテクチャです。
モダンPetstoreAPIは、共有されたコアからREST、GraphQL、gRPCを実装しています。同じペットストアロジックが、一貫した動作でこれら3つのプロトコルすべてにサービスを提供します。
このガイドでは、モダンPetstoreAPIを参照しながら、マルチプロトコルAPIのアーキテクチャ設計、プロトコル層の実装、およびプロトコル間の一貫性の確保方法について学びます。
マルチプロトコルアーキテクチャ
マルチプロトコルAPIは、関心事を層に分離します。
レイヤードアーキテクチャ
┌─────────────────────────────────────────┐
│ Protocol Layer (REST/GraphQL/gRPC) │
├─────────────────────────────────────────┤
│ Application Layer (Use Cases) │
├─────────────────────────────────────────┤
│ Domain Layer (Business Logic) │
├─────────────────────────────────────────┤
│ Data Layer (Database, Cache) │
└─────────────────────────────────────────┘
プロトコル層: HTTP、GraphQLクエリ、gRPC呼び出しを処理します
アプリケーション層: ユースケースを調整します(ペットの作成、注文の実行)
ドメイン層: ビジネスルール(検証、計算)
データ層: 永続化(データベース、キャッシュ)
主要原則
1. プロトコルに依存しないコア
ビジネスロジックはHTTP、GraphQL、gRPCについて知りません。ドメインオブジェクトで動作します。
2. 薄いプロトコルアダプター
プロトコル層は、プロトコル形式とドメインオブジェクトの間で変換を行います。ビジネスロジックは含みません。
3. 共有データモデル
すべてのプロトコルは、内部で同じドメインモデルを使用し、一貫性を保証します。
4. 独立したデプロイ
必要に応じて、各プロトコルを個別にデプロイできます。
共有ドメイン層
ドメイン層には、すべてのプロトコル間で共有されるビジネスロジックが含まれます。
ドメインモデル
// ドメインモデル(プロトコルに依存しない)
class Pet {
id: string;
name: string;
species: Species;
status: PetStatus;
price: number;
constructor(data: PetData) {
this.validate(data);
Object.assign(this, data);
}
validate(data: PetData): void {
if (!data.name || data.name.length < 2) {
throw new ValidationError('Name must be at least 2 characters');
}
if (data.price < 0) {
throw new ValidationError('Price cannot be negative');
}
}
adopt(userId: string): Order {
if (this.status !== PetStatus.AVAILABLE) {
throw new BusinessError('Pet is not available for adoption');
}
this.status = PetStatus.ADOPTED;
return new Order({
petId: this.id,
userId,
total: this.price
});
}
}
このモデルは、REST、GraphQL、gRPCで機能します。検証とビジネスルールは同じです。
ユースケース
// ユースケース(プロトコルに依存しない)
class AdoptPetUseCase {
constructor(
private petRepository: PetRepository,
private orderRepository: OrderRepository
) {}
async execute(petId: string, userId: string): Promise<Order> {
const pet = await this.petRepository.findById(petId);
if (!pet) {
throw new NotFoundError('Pet not found');
}
const order = pet.adopt(userId);
await this.petRepository.save(pet);
await this.orderRepository.save(order);
return order;
}
}
このユースケースは、REST、GraphQL、gRPCのいずれから呼び出されたかに関わらず機能します。
RESTプロトコル層
REST層はHTTPリクエストをドメイン操作に変換します。
RESTコントローラー
// RESTアダプター
class PetsController {
constructor(private adoptPetUseCase: AdoptPetUseCase) {}
async adoptPet(req: Request, res: Response): Promise<void> {
try {
const { petId } = req.params;
const { userId } = req.body;
const order = await this.adoptPetUseCase.execute(petId, userId);
res.status(201).json({
id: order.id,
petId: order.petId,
userId: order.userId,
total: order.total,
status: order.status
});
} catch (error) {
this.handleError(error, res);
}
}
private handleError(error: Error, res: Response): void {
if (error instanceof NotFoundError) {
res.status(404).json({
type: 'https://petstoreapi.com/errors/not-found',
title: 'Not Found',
status: 404,
detail: error.message
});
} else if (error instanceof ValidationError) {
res.status(400).json({
type: 'https://petstoreapi.com/errors/validation-error',
title: 'Validation Error',
status: 400,
detail: error.message
});
} else {
res.status(500).json({
type: 'https://petstoreapi.com/errors/internal-error',
title: 'Internal Server Error',
status: 500
});
}
}
}
RESTルート
app.post('/v1/pets/:petId/adopt', (req, res) =>
petsController.adoptPet(req, res)
);
GraphQLプロトコル層
GraphQL層はGraphQLクエリをドメイン操作に変換します。
GraphQLスキーマ
type Pet {
id: ID!
name: String!
species: Species!
status: PetStatus!
price: Float!
}
type Order {
id: ID!
petId: ID!
userId: ID!
total: Float!
status: OrderStatus!
}
type Mutation {
adoptPet(petId: ID!, userId: ID!): Order!
}
GraphQLリゾルバー
// GraphQLアダプター
const resolvers = {
Mutation: {
adoptPet: async (
_parent: any,
args: { petId: string; userId: string },
context: Context
): Promise<Order> => {
try {
return await context.adoptPetUseCase.execute(
args.petId,
args.userId
);
} catch (error) {
throw new GraphQLError(error.message, {
extensions: {
code: error instanceof NotFoundError ? 'NOT_FOUND' :
error instanceof ValidationError ? 'BAD_USER_INPUT' :
'INTERNAL_SERVER_ERROR'
}
});
}
}
}
};
GraphQLリクエスト
mutation {
adoptPet(
petId: "019b4132-70aa-764f-b315-e2803d882a24"
userId: "user-123"
) {
id
total
status
}
}
gRPCプロトコル層
gRPC層はProtocol Bufferメッセージをドメイン操作に変換します。
Protocol Buffer定義
syntax = "proto3";
package petstore.v1;
service PetService {
rpc AdoptPet(AdoptPetRequest) returns (AdoptPetResponse);
}
message AdoptPetRequest {
string pet_id = 1;
string user_id = 2;
}
message AdoptPetResponse {
string order_id = 1;
string pet_id = 2;
string user_id = 3;
double total = 4;
string status = 5;
}
gRPCサービス実装
// gRPCアダプター
class PetServiceImpl implements IPetService {
constructor(private adoptPetUseCase: AdoptPetUseCase) {}
async adoptPet(
call: ServerUnaryCall<AdoptPetRequest, AdoptPetResponse>,
callback: sendUnaryData<AdoptPetResponse>
): Promise<void> {
try {
const { petId, userId } = call.request;
const order = await this.adoptPetUseCase.execute(petId, userId);
callback(null, {
orderId: order.id,
petId: order.petId,
userId: order.userId,
total: order.total,
status: order.status
});
} catch (error) {
callback({
code: error instanceof NotFoundError ? status.NOT_FOUND :
error instanceof ValidationError ? status.INVALID_ARGUMENT :
status.INTERNAL,
message: error.message
});
}
}
}
モダンPetstoreAPIがマルチプロトコルをどのように実装しているか
モダンPetstoreAPIは、実際の例でマルチプロトコルアーキテクチャを示しています。
アーキテクチャ概要
Modern PetstoreAPI
├── Domain Layer (ドメイン層)
│ ├── Pet (エンティティ)
│ ├── Order (エンティティ)
│ └── Use Cases (ユースケース)
│ ├── CreatePet (ペットの作成)
│ ├── AdoptPet (ペットの引き取り)
│ └── PlaceOrder (注文の実行)
├── REST Layer (REST層)
│ ├── /v1/pets
│ ├── /v1/orders
│ └── OpenAPI 3.2 spec
├── GraphQL Layer (GraphQL層)
│ ├── Query resolvers (クエリリゾルバー)
│ ├── Mutation resolvers (ミューテーションリゾルバー)
│ └── GraphQL schema
└── gRPC Layer (gRPC層)
├── PetService
├── OrderService
└── .proto definitions
一貫したデータモデル
すべてのプロトコルは同じデータを返します。
REST:
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Fluffy",
"species": "CAT"
}
GraphQL:
{
"data": {
"pet": {
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Fluffy",
"species": "CAT"
}
}
}
gRPC:
{
pet_id: "019b4132-70aa-764f-b315-e2803d882a24"
name: "Fluffy"
species: CAT
}
同じデータ、異なる形式。
共有検証
検証はドメイン層で行われるため、すべてのプロトコルで同じルールが適用されます。
// ドメイン検証(共有)
if (name.length < 2) {
throw new ValidationError('Name must be at least 2 characters');
}
RESTが返すもの:
{
"type": "https://petstoreapi.com/errors/validation-error",
"status": 400,
"detail": "Name must be at least 2 characters"
}
GraphQLが返すもの:
{
"errors": [{
"message": "Name must be at least 2 characters",
"extensions": {"code": "BAD_USER_INPUT"}
}]
}
gRPCが返すもの:
code: INVALID_ARGUMENT
message: "Name must be at least 2 characters"
同じ検証、プロトコル固有のエラー形式。
Apidogを使用したマルチプロトコルAPIのテスト
Apidogは、3つのプロトコルすべてをテストすることをサポートしています。
RESTエンドポイントのテスト
POST https://petstoreapi.com/v1/pets/019b4132-70aa-764f-b315-e2803d882a24/adopt
Content-Type: application/json
{
"userId": "user-123"
}
GraphQLミューテーションのテスト
mutation {
adoptPet(
petId: "019b4132-70aa-764f-b315-e2803d882a24"
userId: "user-123"
) {
id
total
}
}
gRPCサービスのテスト
grpc.petstore.v1.PetService/AdoptPet
{
"pet_id": "019b4132-70aa-764f-b315-e2803d882a24",
"user_id": "user-123"
}
一貫性の検証
3つのプロトコルすべてが同じデータを返すことをテストします。
- RESTエンドポイントを呼び出す
- GraphQLミューテーションを呼び出す
- gRPCサービスを呼び出す
- 結果を比較する
Apidogは、このクロスプロトコルテストを自動化できます。
デプロイ戦略
戦略1: シングルサービス
すべてのプロトコルを1つのサービスにデプロイします。
┌─────────────────────────┐
│ PetstoreAPI Service │
│ ├── REST (port 8080) │
│ ├── GraphQL (port 8081)│
│ └── gRPC (port 50051) │
└─────────────────────────┘
利点: シンプルなデプロイ、リソースの共有
欠点: すべてのプロトコルが一緒にスケーリングする
戦略2: 個別のサービス
各プロトコルを個別にデプロイします。
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ REST Service │ │ GraphQL Svc │ │ gRPC Service │
│ (port 8080) │ │ (port 8081) │ │ (port 50051) │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
└─────────────────┴──────────────────┘
│
┌──────────────┐
│ Shared Core │
│ Library │
└──────────────┘
利点: 独立したスケーリング、プロトコル分離
欠点: デプロイがより複雑になる
戦略3: APIゲートウェイ
APIゲートウェイを使用して、プロトコル固有のバックエンドにルーティングします。
┌─────────────┐
│ API Gateway │
└─────────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌───────▼──────┐ ┌────▼─────┐ ┌──────▼──────┐
│ REST Backend │ │ GraphQL │ │ gRPC Backend│
└──────────────┘ └──────────┘ └─────────────┘
利点: 一元化されたルーティング、レート制限、認証
欠点: 追加のレイテンシ、ゲートウェイの複雑さ
モダンPetstoreAPIは、シンプルさのために戦略1を使用しています。
結論
マルチプロトコルAPIは、ビジネスロジックを重複させることなくクライアントに柔軟性を提供します。プロトコル層をドメインロジックから分離することで、共有コアからREST、GraphQL、gRPCをサポートできます。
モダンPetstoreAPIは、一貫したデータモデル、共有検証、プロトコル固有のアダプターを備えたこのアーキテクチャを示しています。クライアントがシンプルさのためにRESTを使用しようと、柔軟性のためにGraphQLを使用しようと、パフォーマンスのためにgRPCを使用しようと、同じビジネスルールで同じペットストアにアクセスできます。
プロトコル間の一貫性を確保するために、ApidogでマルチプロトコルAPIをテストしてください。マルチプロトコルアーキテクチャの実際を見るには、モダンPetstoreAPIを探索してください。
FAQ
3つのプロトコルすべてをサポートする必要がありますか?
いいえ。パブリックAPIではRESTから始めましょう。クライアントが柔軟なデータ取得を必要とする場合はGraphQLを追加し、内部マイクロサービスにはgRPCを追加します。明確なユースケースがある場合にのみプロトコルを追加してください。
プロトコル間の一貫性をどのように保ちますか?
ビジネスロジックをドメイン層で共有します。プロトコルアダプターは形式の変換のみを行い、ビジネスルールを含めてはいけません。すべてのプロトコルをテストして、同じデータを返すことを確認します。
プロトコルを個別にバージョン管理できますか?
はい。RESTがv2である間、GraphQLはv1であることができます。しかし、これは複雑さを生みます。可能な限りプロトコルを同期させるように努めてください。
プロトコル全体で認証をどのように処理しますか?
すべてのプロトコルで同じ認証を使用します。RESTはヘッダーにBearerトークンを使用します。GraphQLも同じトークンを使用します。gRPCはメタデータを使用します。ドメイン層は同じ方法でトークンを検証します。
WebSocketとSSEについてはどうですか?
WebSocketとSSEはAPIプロトコルではなく、トランスポートプロトコルです。リアルタイム更新のために、REST/GraphQL/gRPCと並行してこれらを追加できます。モダンPetstoreAPIには両方が含まれています。
マルチプロトコルAPIをどのようにドキュメント化しますか?
RESTにはOpenAPI、GraphQLにはGraphQLスキーマ、gRPCには.protoファイルを使用します。モダンPetstoreAPIは、https://docs.petstoreapi.com/ですべてを提供しています。
異なるプロトコルに異なるデータベースを使用できますか?
はい、しかし一貫性の課題が生じます。すべてのプロトコルで同じデータ層を使用する方が良いでしょう。プロトコルアダプターに形式の違いを処理させます。
マルチプロトコルAPIをどのようにテストしますか?
Apidogを使用して、1つのツールで3つのプロトコルすべてをテストします。プロトコル間の一貫性を検証するテストスイートを作成します。REST、GraphQL、gRPCが同じ操作に対して同じデータを返すことをテストします。
