このブログ記事ではREST API, GraphQL, gRPCなどのAPIアーキテクチャについて紹介します。

私たちがこれまで学んできたプロトコルスタックの上に構築されたアプリケーションインターフェース、特にプログラム可能なものは **アプリケーションプログラミングインターフェース(API)**と呼ばれ、インターネット上のサービス間の通信を促進します。 自分のサービスに対してAPIを設計する際、第三者が背景知識を持っていなくても簡単に理解し、使用できるように、 特定のルールや制約に従うことが重要です。この記事では、さまざまなタイプのサービスに対して一般的なAPIアーキテクチャをいくつか取り上げます。
REST API
REST(Representational State Transfer)は、おそらく最もシンプルなAPIアーキテクチャであり、HTTPをいくつかの制約と共に使用します。 REST APIでは、リクエストは独立したリソースに対する操作に対応する必要があり、これらの操作は 冪等 である必要があります。つまり、 操作が他の操作の結果に影響を与えず、操作の結果がどのような状況でも同じでなければなりません。また、通信は ステートレス でなければならず、 メッセージには操作に必要なすべての情報が含まれていなければなりません。
REST APIには文法的な原則もあります。HTTPメソッドである GET
、POST
、PUT
、DELETE
は、それぞれリソースの取得、作成、更新、削除に対応する必要があります。
さらに、URIは処理するリソースを明確に表す必要があります。REST APIは主に HTTP 1.1 を使用し、リクエストおよびレスポンスにおいて状態をエンコードするために JSON を使用します。
以下にいくつかの例を示します:
// Bad Example 1
GET /hi HTTP 1.1
Content-Type: application/json
{
"method": "delete",
"item": "product",
"id": 1,
}
// Bad Example 2
GET /hi?method=delete&item=product&id=1 HTTP 1.1
// Good Example
DELETE /product?id=1 HTTP 1.1
最初の悪い例では、GET
リクエストを使用して、/hi
エンドポイントに対してIDが1の製品を削除するためのJSONボディを送信していますが、
これは間違いです。なぜなら、GET
リクエストにはボディが含まれてはいけないからです。2番目の例では、すべての情報をURIに含めていますが、
それでも間違いです。なぜなら、GET
ではなく DELETE
を使用すべきであり、また /hi
というURIの名前がリソースを適切に表していないからです。
3番目の例は、DELETE
メソッドを適切に使用し、リソースを表すURIも適切です。
GraphQL
REST APIの潜在的な欠点の一つは、クライアントがリソースにアクセスする方法が限られている点です。 クエリが複数のリソースを必要とする場合、クライアントは複数のエンドポイントにリクエストを送信しなければならないか、 クエリがレスポンスの一部だけを必要とする場合でも、クライアントはすべてのデータを取得する必要があります。 **GraphQL (Graph Query Language)**は、主に HTTP 1.1 を使用して、柔軟なクエリを処理できる単一のエンドポイントを提供することで、 この問題を解決することを目的としています。以下はGraphQLのスキーマ例です:
type Blogger {
id: ID!
name: String
blogs: [Blog]
}
type Blog {
url: String!
blogger: Blogger
}
type Query {
blogs: [Blog]
blogger(id: String!) : Blogger
}
type Mutation {
uploadBlog(url: String): Blog
}
GraphQLでは、スキーマ がオブジェクトに対してフィールドやその型、関係性を定義します。
Query
スキーマは、クライアントがスキーマによって定義されたオブジェクトにどのようにアクセスできるかを定義し、
Mutation
スキーマは、GraphQLでデータがどのように変更できるかを定義します。サーバーはこれらのクエリを解決するために
リゾルバ を実装し、対応するデータを返します。通常、このデータは JSON 形式です。GraphQLはクライアントに対して柔軟で効率的な体験を提供しますが、
開発者は認証やレート制限などの設定において、より多くの作業が必要になる場合があります。
gRPC
前述の2つのアーキテクチャの比較的新しい代替手段として、gRPCがあります。これは、Googleが実装した**Remote Procedure Call (RPC)**です。 RPCとは、クライアントがサーバーで実装された関数を、あたかもクライアント側で定義されたかのように呼び出せる仕組みのことです。 これにより、クライアントにとって直感的なAPIコールが可能になります。Googleは、Protocol Buffers (.proto)というファイル形式を用いてRPCを実装しており、 これはHTTP 2.0をサポートしています。以下は、gRPCで使用されるプロトコルバッファの例です。
syntax = "proto3";
package "blogPackage";
service Blog {
rpc getBlogById(ID) returns (BlogPost)
rpc getAllBlogs(noParams) returns (BlogPosts);
}
message ID {
int32 id = 1;
}
message noParams {}
message BlogPost {
int32 id = 1;
string author = 2;
string content = 3;
}
message BlogPosts {
repeated BlogPost posts = 1;
}
gRPCでは、プロトコルバッファがオブジェクトとAPIコールのスキーマを含んでおり、C++、Python、JavaScriptなど多くの言語にコンパイル可能です。 これにより、サーバーとクライアントの両方が、それぞれのネイティブ言語でアクセスできます。サーバーはAPIコールの実装を提供し、 クライアントはその関数を使って対応するリクエストをサーバーに送信します。以下は、JavaScriptでのgRPCを使用したサーバーとクライアントの例です。 どちらも、上記のプロトコルバッファで定義されたRPCとオブジェクトにアクセス可能です。
const grpc = require("grpc");
const protoLoader = require("@grpc/proto-loader");
const packageDef = protoLoader.loadSync("blog.proto", {}); // load .proto
const grpcObject = grpc.loadPackageDefinition(packageDef); // loading objects definition
const blogPackage = grpc.blogPackage;
// -- Server Code in `server.js` --
const server = new grpc.Server();
server.bind("localhost:4000", grpc.ServerCredentials.createInsecure());
server.addService(blogPackage.Blog.service, {
"getBlogById": getBlogById,
"getAllBlogs": getAllBlogs,
});
server.start();
function getBlogById (call, callback) {
...
};
function getAllBlogs (call, callback) {
...
}
// -- Client Code in `client.js` --
const client = new blogPackage.Blog("localhost:4000", grpc.credentials.createInsecure());
client.getBlogById({
"id": 1
}, (err, response) => {
...
})
client.getAllBlogs({}, (err, response) => {
...
})
プロトコルバッファは、リクエストやレスポンスをバイナリ形式にシリアライズできるだけでなく、HTTP 2.0を使用することで、 HTTP 2.0の圧縮や多重化の機能を活用し、通信をさらに高速化します。REST APIやGraphQLとは異なり、gRPCは双方向のストリーミングも可能です。 (詳しくは、Nasser, H.による gRPC Crash Course - Modes, Examples, Pros & Cons and moreをご覧になることをお勧めします)。 その効率性と使いやすさから、多くのマイクロサービスがgRPCを採用しています。ただし、ほとんどのウェブブラウザはgRPCをサポートしていません。
WebSocket
gRPCがウェブブラウザで使用できない場合、ブラウザとウェブサーバーの間で双方向通信を確立するにはどうすればよいでしょうか? その解決策がWebSocketです。これにより、接続を開いて、サーバーとクライアントの両方がメッセージを送受信できます。WebSocket接続を確立するには、 クライアントとサーバーがWebSocketハンドシェイクを行い、リクエストとレスポンスを交換します。
// Client Request
GET / HTTP 1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
...
// Server Response
HTTP 1.1 101 Switching Protocols
Upgrade: websocket
Connection Upgrade
...
WebSocket接続はステートフルであるため、クライアントとサーバーは接続の状態を維持する必要があります。 これにより、ウェブサービスを横にスケーリングする際(同じウェブサーバーとの接続を維持する必要があるか、ウェブサーバー間で通信を設定する必要があります)には注意が必要です。 しかし、WebSocketを使用すれば、マーケットプレイス、ライブフィード、ゲームなど、リアルタイムデータを必要とするさまざまなウェブアプリケーションを簡単に作成できます。
Webhooks
ウェブサーバーが、Stripeのような時間のかかるサービスにリクエストを送信する際、リクエストがタイムアウトするまでに待機できる時間は限られています。 この問題を回避するため、サービスに対して繰り返しリクエストを送り、処理状況が完了するまでサービスからのステータスを受け取る方法があります。 これをポーリングと呼びます。(ポーリングはWebSocketの代替手段にもなり得ます。)ただし、この方法では複数のリクエストを行う必要があり、 ウェブサーバーとサービスの両方にコストがかかります。
この問題に対する解決策がWebhookです。クライアントはレスポンスを受け取るためのエンドポイントを設定し、 それをリクエストの一部として送信します。サーバーが処理を終了すると、WebhookのURLを使用して、 別のHTTPメッセージとしてクライアントにレスポンスを送信できます。メッセージが正常に受信されたことを確認するために、 ポーリングをバックアップとして設定し、クライアントがレスポンスを受け取るまで定期的にリクエストを送信するのが一般的です。
結論
この記事ではいくつかのAPIアーキテクチャを紹介しましたが、どのように選択すればよいでしょうか?シンプルなウェブアプリケーションを素早く構築したい場合はREST APIを選び、 クライアントに柔軟なクエリ機能を提供したい場合はGraphQLを選ぶと良いでしょう。リアルタイムデータが必要なウェブアプリケーションの場合、ポーリングやWebSocketを検討することができます。 APIの処理に時間がかかる場合、Webhookを利用することが有効です。最後に、マイクロサービスで高速かつ柔軟な通信が必要な場合は、gRPCを検討すべきでしょう。
リソース
- Nasser, H. 2019. WebSockets Crash Course - Handshake, Use-cases, Pros & Cons and more. YouTube.