【REST API / gRPC / tRPC】徹底比較!どれを選ぶべきか?
はじめに
API設計のアーキテクチャスタイルには、REST API、GraphQL、gRPC、tRPCなど、複数の選択肢があります。
私はこれまでに、実務や個人開発でREST API、gRPC、tRPCを使用してきました。その上で今回は、それぞれの特徴と開発の流れについて紹介します。さらに、それぞれのメリット・デメリットを挙げ、特定のケースに適した選択について考察してみました。
API設計のアーキテクチャスタイルの選択に悩んでいる方に、少しでも参考になれば幸いです。
REST API
RESTとは
まずRESTとはシンプルなWEB全体のアーキテクチャスタイルのことです。RESTには4つの原則があり(6つとも言われている)それらを満たすもののことをRESTfulと言います。
※それぞれの原則の詳細は省略します。
- 統一インターフェース
- アドレス可能性
- 接続性
- ステートレス性
REST APIはRESTの原則に従って設計されたAPIのことです。
HTTPプロトコルを基盤にしており、リソースを一意の識別子(URL)で指定し、HTTPメソッド(GET、POST、PUT、DELETE)を使用して操作します。
データのやり取りはJSONやXML、プレーンテキストなどさまざまなフォーマットで行います。
REST APIの特徴
- リソース指向: リソース(データオブジェクト)をURLによって識別します。
- HTTPメソッドの利用:HTTPメソッド(GET、POST、PUT、DELETEなど)を使用して、リソースの操作を行います。これにより、CRUD操作(データの取得、作成、更新、削除といった操作)を直感的に行うことができます。
- 多言語サポート:HTTPを利用するため、ほぼすべてのプログラミング言語で実装が可能です。Python、JavaScript、Java、C#, PHP、Ruby、Goなど、あらゆる言語でRESTfulなサービスを構築でき、言語に依存しない柔軟なインタフェースを提供します。
- シンプルで広く採用:シンプルな設計で、既にWeb開発に携わっているエンジニアにとっては直感的であり、多くのwebサービスやアプリケーションで幅広く採用されています。
REST APIの開発の流れ
- APIのスキーマを設計する
- バックエンドでAPIのロジックを実装する
- フロントエンドでAPIを呼び出す実装をする
記事投稿に関するAPIをREST APIで示すと、以下のようになります。
- APIのスキーマを設計する
エンドポイントを設計し、HTTPメソッドやリクエスト・レスポンスフォーマットを定義します。
CRUD操作 | URL | HTTPメソッド |
---|---|---|
全記事を取得 | /posts |
GET |
特定の記事を取得 | /posts/1 |
GET |
記事の作成 | /posts |
POST |
記事の更新 | /posts/1 |
PUT |
記事の削除 | /posts/1 |
DELETE |
スキーマ定義にはOpenAPIなどのツールを使用するとフォーマットが統一されて管理しやすくなります。
OpenAPI
OpenAPIとはREST API の API 記述形式です。
Swagger Editorにコピペするとドキュメントが出来上がります。
openapi: 3.0.0
info:
title: Blog API
version: 1.0.0
paths:
/posts/{id}:
get:
summary: "Get a specific post"
parameters:
- in: path
name: id
required: true
schema:
type: integer
description: "ID of the post to retrieve"
responses:
'200':
description: "A single post"
content:
application/json:
schema:
$ref: '#/components/schemas/Post'
'404':
description: "Post not found"
components:
schemas:
Post:
type: object
properties:
id:
type: integer
title:
type: string
content:
type: string
required:
- id
- title
- content
- バックエンドでAPIのロジックを実装する
例えばNode.jsとexpressを用いた場合のイメージです。
// GET /posts/:id - 特定の記事を取得する
app.get('/posts/:id', (req, res) => {
const postId = req.params.id;
// idを元に特定の記事を取得
const post = posts.find(p => p.id === postId);
if (post) {
res.status(200).json(post);
} else {
res.status(404).json({ message: 'Post not found' });
}
});
/posts/:id
というURLに対してGET
リクエストが来た場合に呼び出されます。
パスパラメーターのid
をキーに一意の記事を検索してレスポンスを返します。
- フロントエンドでAPIを呼び出す実装をする
例えばNext.jsとaxiosを用いた場合のイメージです。
useEffect(() => {
if (id) {
fetchPostById(id);
}
}, [id]);
const fetchPostById = async (postId) => {
try {
const response = await axios.get(`/posts/${postId}`);
setPost(response.data);
} catch (err) {
console.log(err)
};
/posts/:id
というURLに対してGET
リクエストをし、レスポンスとして一意の記事が返されます。
gRPC
RPCとは
RPC(Remote Procedure Call)は、「遠隔手続き呼び出し」と訳され、手続き(プロシージャ)を遠隔で呼び出すための技術です。これは一般的な関数を呼び出すようなイメージで、別のサーバーやクライアントから各サーバーが提供する関数を呼び出す仕組みです。
RPCは通信方法を示す抽象的な概念であり、通信規約(RPCプロトコル)や、やり取りするデータ形式が異なるさまざまな種類のRPCがあります。以下は代表的なRPCの種類です。
- gRPC
- tRPC
- JSON-RPC
- XML-RPC
gRPCは、Googleによって開発されたRPCフレームワークです。gRPCは通信プロトコルとしてHTTP/2を使用し、Protocol Buffersという形式で効率よくデータをやり取りします。
Protocol Buffersとは
Protocol BuffersはGoogleが開発したインタフェース記述言語(IDL)です。
データをバイナリ形式に変換することで、テキスト形式に比べて格段にコンパクトかつ高速なデータ転送を実現します。
Protocol Buffersのコンパイラであるprotoc
は、.protoファイルに定義されたAPIスキーマをもとに、さまざまなプログラミング言語に対応したクライアントやサーバーのコードを自動生成します。
gRPCの特徴
- 多言語サポート:gRPCは、多くのプログラミング言語をサポートしており、C++, Go, Python, Ruby, JavaScript, Java, C#, Objective-C, PHPなど、様々な言語でクライアントやサーバーを実装できます。
-
型安全性:
.proto
ファイルのスキーマ定義を基に自動生成されたコードによって各言語における型安全性を保証します。 - 高速で効率的な通信:HTTP/2プロトコルを使用して通信を行っており、ヘッダー圧縮やストリーミング、双方向通信を活用することで、より効率的で高速なデータ転送を実現します。
- 双方向ストリーミング:クライアントとサーバーの間で双方向のストリーミング通信をサポートしています。これにより、リアルタイムでのデータ送受信や効率的な通信が可能です。
gRPCの開発の流れ
-
.proto
ファイルにAPIのスキーマを定義をする -
protoc
コマンドを使って、.proto
ファイルからバックエンド言語用のコードを生成 - バックエンドでAPIのロジックを実装
-
.proto
ファイルからフロントエンド言語用のコードを生成 - フロントエンドでAPIを呼び出す実装をする
記事投稿に関するAPIをgRPCで示すと、以下のようになります。
- 記事の取得
メソッド名:GetPost
リクエストデータ形式:バイナリ形式(Protocol Buffers)
-
.proto
ファイルにAPIのスキーマを定義をする
service BlogService {
rpc GetPost(GetPostRequest) returns (GetPostResponse);
}
message GetPostRequest {
int32 id = 1;
}
message GetPostResponse {
int32 id = 1;
string title = 2;
string content = 3;
}
GetPost
メソッドのリクエストの型とレスポンスの型をあらかじめ定義しておきます。
-
protoc
コマンドを使って、.proto
ファイルからバックエンド言語用のコードを生成
.proto
ファイルのスキーマ定義をもとに例としてGo言語用のコードを自動生成します。
自動生成されたコードの中には定義したクラスやインターフェース、定義したメソッドを呼び出すためのコードが含まれます。
protoc --go_out=. --go-grpc_out=. blog.proto
- バックエンドでAPIのロジックを実装
生成したコードを使ってGoでAPIのロジックを実装したイメージです。
// GetPostメソッドの実装
func (s *server) GetPost(ctx context.Context, req *pb.GetPostRequest) (*pb.GetPostResponse, error) {
post := &pb.GetPostResponse{
Id: req.Id,
Title: "Sample Title",
Content: "Sample Content",
}
return post, nil
}
-
.proto
ファイルからフロントエンド言語用のコードを生成
ブラウザから直接使用することができずフロントエンドで使用する場合はgrpc-webなどのライブラリを別途インストールする必要があります。
protoc
コマンドを実行し、.proto
ファイルからTypescript言語用のコードを生成するイメージです。
protoc \
--js_out=import_style=commonjs,binary:./generated \
--grpc-web_out=import_style=typescript,mode=grpcwebtext:./generated \
blog.proto
- フロントエンドでAPIを呼び出す実装をする
grpc-webクライアントを使用してAPIを呼び出す部分のイメージです。
client.GetPost({ id: 1 }, (error: any, response: any) => {
if (error) {
console.error('Error fetching post:', error);
} else {
setPost(response);
}
});
tRPC
tRPCは、TypeScript向けのRPCフレームワークで、TypeScriptの型システムを活用してクライアントとサーバー間の通信を簡単かつ型安全に行うことができます。
tRPCの特徴
- エンドツーエンドの型安全性:tRPCは、クライアントとサーバー間でTypeScriptの型を共有することで、フロントエンドとバックエンドで型安全な開発が可能です。
- コード自動生成が不要:バックエンドで直接APIスキーマを定義するため、コードの自動生成が必要ありません。そのため、すぐに使用でき、開発の手間を大幅に軽減できます。
- エディタの補完機能を最大限に活用:TypeScriptの型情報を基に、エディタでの補完機能やコードジャンプ機能がフルに活用できます。これにより、開発中のコーディング効率が大幅に向上します。
- コードの一貫性と可読性:サーバーとクライアントが同じコードベースで型を共有するため、一貫性のある開発が可能です。特に、大規模なプロジェクトでのメンテナンス性が向上します。
tRPCの開発の流れ
- バックエンドでAPIのスキーマを定義しロジックを実装する
- フロントエンドでAPIを呼び出す実装をする
記事投稿に関するAPIをtRPCで示すと、以下のようになります。
- 記事の取得
メソッド名:GetPost
リクエストデータ形式:JSON
- バックエンドでAPIのスキーマを定義しロジックを実装する
Zodというスキーマ定義ライブラリを用いたイメージです。
getPost: t.procedure
.input(z.object({ id: z.number() })) // 直接スキーマを定義
.query(async ({ input }) => {
const { id } = input;
const post = await db.getPostById(id);
return {
id: post.id,
title: post.title,
content: post.content,
};
}),
- フロントエンドでAPIを呼び出す実装をする
const post = await client.blog.getPost.query({ id: postId });
バックエンドで作成したAPIを関数を呼ぶように使用することができます。
REST API vs gRPC vs tRPC
REST API、gRPC、tRPCはそれぞれ異なる特徴を持つAPI設計方法です。どれを採用するかは、プロジェクトの要件や技術スタック、チームのスキルセットによって異なりますが、それぞれの開発を体験した上で個人的に感じたメリット・デメリットや、採用の決め手について考えてみました。
REST APIのメリット・デメリット
メリット
- シンプルで学習コストが低い:ツールやライブラリが豊富なのがありがたいです。リソース指向で、Web設計に基づいているため、ブログなど、一意のリソース(データオブジェクト)を作成・更新・削除する場合に適しています。リアルタイム性のないアプリケーションには基本的にREST APIで十分だと感じました。
デメリット
- 型管理をするのが面倒:APIの型付けのためにtype.tsファイルなどを用意しないといけないのがかなりネックだと感じます。(おそらく型付けを自動生成してくれるいい方法あります)
- APIのバージョン管理が煩雑:実装と設計書の乖離が発生しやすいです。いつの間にかAPI仕様が変更され、フロントエンドが最新に追いついておらずデグレする羽目になることがあります。(これもおそらくいい方法あります)
- バイナリ形式と比べるとパフォーマンスに制約が生じる:JSON形式でデータをやり取りするため、gRPCのバイナリ形式と比べてデータサイズが大きくなり、パフォーマンスに制約が生じることがありますが、個人的にはあまり感じたことはありません。
gRPCのメリット・デメリット
メリット
- 高速かつリアルタイム通信に強い:高速なデータ転送が可能で、リアルタイム通信を必要とするサービス(リアルタイムチャットアプリやオンラインゲームなど)に適しています。
- 多言語でも型安全な開発が可能:多言語でも自動生成コードによって型の一貫性が保証されます。
デメリット
- コード自動生成が割と手間:APIの変更があるたびにコードの自動生成が必要なため、フロントエンドの開発者にとっては手間が増えます。
-
学習コストが高い:
.proto
ファイルの読み方、書き方や自動生成したコードの使い方など慣れるまで大変です。 - ブラウザから直接使用できない:javascriptで使用するためにはgRPC-Webのような追加のライブラリが必要です。
tRPCのメリット・デメリット
メリット
- 開発体験が良い:TypeScriptを使用したプロジェクトに適しており、コードジャンプ機能や型補完機能によって開発体験が非常に向上します。
- APIの変更が容易で開発速度が向上:サーバーとクライアントが同じコードベースで型を共有できます。これにより、APIの変更がクライアント側にも即座に反映され開発速度が向上します。
デメリット
- Typescriptの知見が必要:TypeScript以外の言語では利用できません。
- ドキュメントが少ない:比較的新しい技術であるため、ツールやライブラリのエコシステムが他と比べて充実していない点があります。
それぞれの採用の決め手
学習コストをかけず、リアルタイム性が求められないアプリケーションで小〜大規模プロジェクト
→REST API
高性能なリアルタイム通信が必要で、フロントエンドとバックエンドで異なる言語で型安全に開発を行いたい大規模プロジェクト
→gRPC
TypeScriptの知見があり、型安全性と開発速度を重視したい小〜中規模プロジェクト
→tRPC
まとめ
今回は、REST API、gRPC、tRPCの特徴、開発の流れ、そして私が個人的に感じたメリットとデメリットを紹介しました。
これまではシンプルさからREST APIを採用することが多かったのですが、TypeScriptでの型管理が面倒であることがネックでした。今後、Next.jsとTypeScriptを使用する私としては、開発速度や開発体験の良さからtRPCを採用していきたいと思っています。
Next.jsでtRPCを含む雛形プロジェクトをすぐに作成できるT3スタックというCLIツールがあるので、これをさらに深掘りしていこうと思います。T3スタックを誰でも簡単に始められるための手順や雛形プロジェクトの中身については、以前の記事にまとめたので、気になる方はぜひご覧ください!
以上、主観強めにはなりましたが、API設計のアーキテクチャスタイルの選択に悩んでいる方に少しでも参考になれば幸いです!
Discussion
めっちゃ参考になります。良質な記事をありがとうございます。
とても励みになります!ありがとうございます!🙌