この記事は、API通信を 基礎知識ゼロから実務で通用するところまで 学ぶシリーズ(全4回)の 第3回 です。同じ「ToDo API」を REST・gRPC・Connect の3方式で作りながら、仕組み・実装・実務での運用を順に解説します。
| 回 | 内容 | 章 |
|---|---|---|
| 第1回 | Web通信の基礎とREST | 00〜03 |
| 第2回 | gRPCとProtocol Buffers | 04〜06 |
| 第3回 | Connect | 07〜09 |
| 第4回 | 技術選定とミドルへのステップアップ | 10〜11・用語集 |
はじめに
第2回では gRPC を実装し、「ブラウザから直接呼べない」「curl で叩けない」「TypeScript だと型が付きにくい」といった弱点を確認しました。第3回では、それらを解消する Connect(connect-es v2)の仕組みと実装、そして実務への導入手順を扱います。
この記事では、第2回で作った todo.proto をそのまま使います。第2回を読んでいない方のために、以下に再掲します。
proto/todo/v1/todo.proto(第2回と同じ)
syntax = "proto3"; // proto の文法バージョン
package todo.v1; // 名前空間。v1 はAPIのバージョン
// ---- データの形(message) ----
message Todo {
string id = 1; // 「= 1」は値ではなく「フィールド番号」
string title = 2;
bool done = 3;
}
// 各RPCは専用のリクエスト/レスポンス型を持つ(後から項目を足せるように)
message CreateTodoRequest { string title = 1; }
message CreateTodoResponse { Todo todo = 1; }
message GetTodoRequest { string id = 1; }
message GetTodoResponse { Todo todo = 1; }
message ListTodosRequest {
int32 page_size = 1;
string page_token = 2; // 03章のカーソルと同じ考え方
}
message ListTodosResponse {
repeated Todo todos = 1; // repeated = 配列
string next_page_token = 2;
}
message WatchTodosRequest {}
message WatchTodosResponse { Todo todo = 1; }
// ---- 関数の一覧(service) ----
service TodoService {
rpc CreateTodo(CreateTodoRequest) returns (CreateTodoResponse);
rpc GetTodo(GetTodoRequest) returns (GetTodoResponse) {
option idempotency_level = NO_SIDE_EFFECTS; // 副作用なし(=安全)と宣言
}
rpc ListTodos(ListTodosRequest) returns (ListTodosResponse) {
option idempotency_level = NO_SIDE_EFFECTS;
}
// stream = サーバーから何件も続けて送る(サーバーストリーミング)
rpc WatchTodos(WatchTodosRequest) returns (stream WatchTodosResponse);
}
動作環境
コードはすべて TypeScript(Node.js 20 以上)で書いています。ライブラリはそれぞれ次のメジャーバージョンを前提にしています。
- Express 5 / zod
- @grpc/grpc-js / @grpc/proto-loader
- @connectrpc/connect v2 / @bufbuild/protobuf v2 / Buf CLI
メジャーバージョンが変わると API が変わることがあるので、実装するときは公式ドキュメントも併せて確認してください。
07 Connectのしくみ
Connect は Buf 社が開発した RPC フレームワークで、現在は CNCF(クラウドネイティブ技術の財団)のプロジェクトです。一言でいうと「gRPC と同じ .proto を使いながら、普通の HTTP として curl やブラウザから叩ける」仕組みです。
この章のゴール
- Connect が gRPC の何を解決したのか説明できる
- Connect プロトコルの通信を生の HTTP として読める
- REST・gRPC・Connect の通信の違いを並べて説明できる
1つのサーバーが3つのプロトコルを話す
Connect のサーバーは、次の3つのプロトコルを同時に受け付けます。どれで来たかは Content-Type ヘッダーで自動判別されます。
-
Connect プロトコル
Connect 独自の、シンプルな HTTP ベースのプロトコル。HTTP/1.1 でも動き、JSON でも送れる。ブラウザや curl 向け。 -
gRPC
本物の gRPC。既存の gRPC クライアント(Go や Java のサービス、grpcurl)からそのまま呼べる。 -
gRPC-Web
06章のプロトコル。Envoy なしでサーバーが直接話せる。
つまり、Envoy のような変換プロキシが不要になり、フロントエンド(ブラウザ)もバックエンド(他のマイクロサービス)も同じサーバーを直接呼べます。
Connect プロトコルの中身
Unary(1対1)の呼び出しは、驚くほど普通の HTTP リクエストです。
Connect プロトコル(JSON)の成功例
POST /todo.v1.TodoService/CreateTodo HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Connect-Protocol-Version: 1
Connect-Timeout-Ms: 3000
{"title":"牛乳を買う"}
HTTP/1.1 200 OK
Content-Type: application/json
{"todo":{"id":"7f3a...","title":"牛乳を買う"}}
Connect プロトコルの失敗例
HTTP/1.1 404 Not Found
Content-Type: application/json
{"code":"not_found","message":"todo nope は存在しません"}
gRPC と比べたときの違いを整理します。
- 5バイトの枠がない:Unary ではボディがそのままメッセージ。
- トレーラーを使わない:エラーは適切な HTTP ステータス+JSON ボディで返る。だからブラウザでもエラーが読めるし、監視ツールも 4xx/5xx を正しく数えられる。
-
JSON でも protobuf バイナリでも送れる:
Content-Type: application/jsonなら JSON、application/protoならバイナリ。開発中は JSON で読みやすく、本番はバイナリで効率よく、と切り替えられる。 - HTTP/1.1 でも動く:HTTP/2 を必須としないので、既存のインフラ(LB、CDN、プロキシ)にそのまま乗る。
-
GET にもできる:
idempotency_level = NO_SIDE_EFFECTSを付けたメソッドは GET で呼べるので、CDN やブラウザでキャッシュできる。
JSON の出力で「false」が消える
上の成功例のレスポンスに "done": false がないことに気づきましたか? protobuf の JSON 形式では初期値(false、0、空文字)は省略されます。04章の「値なしと初期値が区別できない」問題と同じ話です。生成コードを使うクライアントは自動で初期値を補うので問題ありませんが、JSON を手で読む場合は知っておきましょう。また JSON のキーは proto のフィールド名を lowerCamelCase にしたもの(next_page_token → nextPageToken)です。
同じ操作を3方式で見比べる
「ToDoを1件取得して、見つからなかった」ときの通信を並べます。
REST の場合
GET /todos/nope HTTP/1.1
Accept: application/json
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{"title":"Not Found","status":404,"detail":"todo nope は存在しません"}
URLとメソッドは人間が設計する。エラー形式もチームごとに決める。
gRPC の場合
:method: POST
:path: /todo.v1.TodoService/GetTodo
content-type: application/grpc+proto
[00][00 00 00 06][0A 04 6E 6F 70 65] ← {id:"nope"} の protobuf
:status: 200 ← 失敗でも 200
content-type: application/grpc+proto
(ボディなし)
grpc-status: 5 ← トレーラー: NOT_FOUND
grpc-message: todo%20nope%20... ← URLエンコードされたメッセージ
HTTP/2 必須、バイナリ、結果はトレーラー。ブラウザや curl からは扱いにくい。
Connect の場合
POST /todo.v1.TodoService/GetTodo HTTP/1.1
Content-Type: application/json
Connect-Protocol-Version: 1
{"id":"nope"}
HTTP/1.1 404 Not Found
Content-Type: application/json
{"code":"not_found","message":"todo nope は存在しません"}
パスは gRPC と同じ規則(設計不要)、でも見た目は普通の HTTP+JSON。エラーは HTTP ステータスにも反映される。
ストリーミングはどうなる?
ストリーミングでは Connect も gRPC と似た「5バイトの枠」を使い、最後に「終了メッセージ」(エラーやトレーラー相当の情報を含む)をボディの中に送ります。トレーラーに頼らないので、ブラウザでも Server streaming を受け取れます。ただしブラウザの fetch はリクエストボディのストリーミングに対応していないため、ブラウザから使えるのは Unary と Server streaming です。Client streaming と双方向は、サーバー同士(HTTP/2)の通信で使います。
Buf CLI:.proto を扱う道具箱
Connect とセットで使われるのが Buf CLI(buf コマンド)です。protobuf 公式の protoc より設定が簡単で、実務に欠かせない機能を持っています。
| コマンド | 役割 |
|---|---|
buf generate |
.proto から各言語のコードを生成する。 |
buf lint |
命名規則やファイル構成のルール違反を検出する(例:RPC ごとに専用の Request/Response 型を使っているか)。 |
buf breaking |
既存の .proto と比較して、互換性を壊す変更(フィールド番号の変更・削除など)を検出する。CI に入れると事故を防げる。 |
buf format |
.proto のコード整形。 |
buf curl |
Connect / gRPC / gRPC-Web のどれでもリクエストを送れる curl 風コマンド。 |
理解度チェック
Q1. Connect のサーバーに、既存の Go の gRPC クライアントから接続できる?
できます。Connect のサーバーは gRPC プロトコルも話すので、通常の gRPC クライアントから呼べます(gRPC は HTTP/2 が必要なので、サーバーを HTTP/2 で起動しておく必要があります)。
Q2. Connect の Unary 呼び出しで、エラーはどう返る?
404 や 400 などの HTTP ステータスと、{"code":"not_found","message":"..."} のような JSON ボディで返ります。トレーラーは使いません。
Q3. JSON モードで通信しているとき、.proto のフィールド名を変更するとどうなる?
JSON ではキー名が使われるので、名前を変えると互換性が壊れます。バイナリなら番号で送るので壊れません。JSON を使う可能性があるなら、フィールド名も変更しない前提で設計します(buf breaking の設定で名前の変更も検出できます)。
08 Connectを実装する
connect-es(v2)で、04章と同じ todo.proto からサーバー・Node クライアント・ブラウザクライアントを作ります。05章の gRPC 実装と見比べると、型が付いたことで何が変わるかが分かります。
この章のゴール
- buf でコードを生成し、型付きのサーバーとクライアントを書ける
- ConnectError でエラーを返し、コードで分岐できる
- インターセプターで認証とログを共通化できる
- 既存の Express アプリに Connect を同居させられる
準備:インストールとコード生成
# 実行時に使うライブラリ
npm i @connectrpc/connect @connectrpc/connect-node @bufbuild/protobuf
# 開発時に使うツール(buf CLI とコード生成プラグイン)
npm i -D @bufbuild/buf @bufbuild/protoc-gen-es
version: v2
modules:
- path: proto # .proto を置くディレクトリ
lint:
use:
- STANDARD # 標準の命名・構成ルール
breaking:
use:
- FILE # 互換性チェックの厳しさ
version: v2
inputs:
- directory: proto
plugins:
- local: protoc-gen-es # TypeScript 用の生成プラグイン
out: src/gen # 出力先
opt: target=ts # .ts ファイルを出力
npx buf lint # ルール違反がないか確認
npx buf generate # → src/gen/todo/v1/todo_pb.ts が生成される
生成された todo_pb.ts には、メッセージの型(Todo, CreateTodoRequest …)、メッセージを作るためのスキーマ(TodoSchema …)、サービスの定義(TodoService)が入っています。このファイルは手で編集しません。.proto を直したら再生成します。
サーバーのコード
import http from "node:http";
import { randomUUID } from "node:crypto";
import { EventEmitter, on } from "node:events";
import { create } from "@bufbuild/protobuf";
import { Code, ConnectError, type ConnectRouter } from "@connectrpc/connect";
import { connectNodeAdapter } from "@connectrpc/connect-node";
import { TodoService, TodoSchema, type Todo } from "./gen/todo/v1/todo_pb.js";
const todos = new Map<string, Todo>();
const bus = new EventEmitter();
// .proto の service を実装する。引数 req と戻り値に型が付いている!
function routes(router: ConnectRouter) {
router.service(TodoService, {
async createTodo(req) {
// req.titel と typo すればコンパイルエラーになる
const title = req.title.trim();
if (!title) throw new ConnectError("title は必須です", Code.InvalidArgument);
const todo = create(TodoSchema, { id: randomUUID(), title, done: false });
todos.set(todo.id, todo);
bus.emit("created", todo);
return { todo }; // 戻り値の形も型チェックされる
},
async getTodo(req, ctx) {
console.log("x-request-id:", ctx.requestHeader.get("x-request-id"));
const todo = todos.get(req.id);
if (!todo) throw new ConnectError(`todo ${req.id} は存在しません`, Code.NotFound);
return { todo };
},
async listTodos(req) {
const size = req.pageSize || 20;
return { todos: [...todos.values()].slice(0, size), nextPageToken: "" };
},
// Server streaming は「非同期ジェネレーター」で書く。yield するたびに1件送られる
async *watchTodos(_req, ctx) {
// ctx.signal はクライアント切断やタイムアウトで中断される
for await (const [todo] of on(bus, "created", { signal: ctx.signal })) {
yield { todo: todo as Todo };
}
},
});
}
// Node 標準の http サーバーに Connect を載せる(HTTP/1.1)
http.createServer(connectNodeAdapter({ routes })).listen(8080, () => {
console.log("Connect server: http://localhost:8080");
});
05章の gRPC 版と比べてみてください。as any が消え、コールバックが async/await と throw になり、ストリーミングは yield になりました。後片付けも ctx.signal と for await の組み合わせで自動的に行われます。
curl で叩く(ここが Connect の強み)
npx tsx src/connect-server.ts
# 普通の curl で、JSON で呼べる
curl -X POST http://localhost:8080/todo.v1.TodoService/CreateTodo \
-H "Content-Type: application/json" \
-d '{"title":"牛乳を買う"}'
# => {"todo":{"id":"...","title":"牛乳を買う"}}
# 存在しないIDは 404 + {"code":"not_found",...}
curl -i -X POST http://localhost:8080/todo.v1.TodoService/GetTodo \
-H "Content-Type: application/json" -d '{"id":"nope"}'
# ストリーミングは buf curl で確認できる(Ctrl+C で終了)
npx buf curl --schema proto \
http://localhost:8080/todo.v1.TodoService/WatchTodos -d '{}'
Node クライアントのコード
import { createClient, ConnectError, Code } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-node";
import { TodoService } from "./gen/todo/v1/todo_pb.js";
// 「トランスポート」= どのプロトコルで、どこに送るか
const transport = createConnectTransport({
baseUrl: "http://localhost:8080",
httpVersion: "1.1",
});
// サービス定義からクライアントを作る。メソッドは全部型付き
const client = createClient(TodoService, transport);
// --- ストリーミングを購読(for await で1件ずつ受け取る) ---
const abort = new AbortController();
(async () => {
try {
for await (const res of client.watchTodos({}, { signal: abort.signal })) {
console.log("[通知] 作成:", res.todo?.title);
}
} catch (e) {
if (ConnectError.from(e).code !== Code.Canceled) console.error(e);
}
})();
// --- Unary(普通の async 関数のように呼べる) ---
const { todo } = await client.createTodo(
{ title: "牛乳を買う" },
{ timeoutMs: 3000, headers: { "x-request-id": crypto.randomUUID() } },
);
console.log("作成:", todo?.id, todo?.title);
try {
await client.getTodo({ id: "nope" }, { timeoutMs: 3000 });
} catch (e) {
const err = ConnectError.from(e); // どんな例外も ConnectError に正規化
if (err.code === Code.NotFound) console.log("見つからない:", err.rawMessage);
else throw err;
}
setTimeout(() => abort.abort(), 500);
プロトコルの切り替えは1行
サーバー同士を gRPC で通信させたいときは、トランスポートを createGrpcTransport({ baseUrl })(@connectrpc/connect-node)に差し替えるだけです。クライアントのコードは一切変わりません。その場合、サーバーは http2.createServer(...) で HTTP/2 として起動します。
ブラウザクライアント
ブラウザでは @connectrpc/connect-web のトランスポートを使います。生成された型も、クライアントの書き方も Node と同じです。
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { TodoService } from "../gen/todo/v1/todo_pb";
export const todoClient = createClient(
TodoService,
createConnectTransport({
baseUrl: "https://api.example.com",
useHttpGet: true, // NO_SIDE_EFFECTS のメソッドを GET で送り、キャッシュ可能にする
}),
);
// 画面側: const { todos } = await todoClient.listTodos({ pageSize: 20 });
React を使う場合は、TanStack Query と統合する Connect-Query(@connectrpc/connect-query)を使うと、キャッシュ・ローディング状態・再取得の管理まで型付きで行えます。
インターセプター:認証とログを共通化
インターセプターは「次の処理(next)を受け取って、新しい処理を返す関数」です。next(req) の前後に共通処理を挟みます。
import { Code, ConnectError, createContextKey, type Interceptor } from "@connectrpc/connect";
// ハンドラーにログインユーザーを渡すための「箱」
export const kUser = createContextKey<{ id: string } | undefined>(undefined);
// ログ:全RPCの所要時間と結果を出す
export const logger: Interceptor = (next) => async (req) => {
const start = performance.now();
try {
const res = await next(req);
console.log(`${req.service.typeName}/${req.method.name} OK ${(performance.now() - start).toFixed(1)}ms`);
return res;
} catch (e) {
const err = ConnectError.from(e);
console.log(`${req.service.typeName}/${req.method.name} ${Code[err.code]} ${(performance.now() - start).toFixed(1)}ms`);
throw e;
}
};
// 認証:トークンを検証し、ユーザー情報をコンテキストに入れる
export const auth: Interceptor = (next) => async (req) => {
const token = req.header.get("authorization")?.replace(/^Bearer /, "");
const user = token ? await verifyToken(token) : undefined; // JWT 検証など
if (!user) throw new ConnectError("ログインが必要です", Code.Unauthenticated);
req.contextValues.set(kUser, user);
return next(req);
};
// サーバー: 配列の順に外側から実行される
connectNodeAdapter({ routes, interceptors: [logger, auth] });
// ハンドラー内: ctx.values から取り出す
// (例:proto に DeleteTodo を追加し、Todo に owner_id を持たせた場合)
async deleteTodo(req, ctx) {
const user = ctx.values.get(kUser);
const todo = todos.get(req.id);
if (todo && todo.ownerId !== user?.id) { // 認可(03章)も忘れずに
throw new ConnectError("権限がありません", Code.PermissionDenied);
}
// ...
}
既存の Express アプリに同居させる
実務では、ゼロから Connect で作るより既存の REST API に少しずつ Connect を足していくケースのほうが多いです。@connectrpc/connect-express を使えば、同じ Express アプリの中に両方を載せられます。
import express from "express";
import cors from "cors";
import { cors as connectCors } from "@connectrpc/connect";
import { expressConnectMiddleware } from "@connectrpc/connect-express";
import { routes } from "./connect-routes.js";
import { restRouter } from "./rest-routes.js";
const app = express();
// ブラウザから呼ぶ場合の CORS。Connect が必要とするヘッダーを許可する
app.use(cors({
origin: ["https://app.example.com"],
methods: [...connectCors.allowedMethods],
allowedHeaders: [...connectCors.allowedHeaders, "Authorization"],
exposedHeaders: [...connectCors.exposedHeaders],
}));
// express.json() は REST 側だけにかける(全体にかけると Connect がボディを読めなくなる)
app.use("/v1", express.json(), restRouter); // 既存の REST はそのまま
app.use(expressConnectMiddleware({ routes })); // Connect を追加
app.listen(8080);
ミドルへの視点:02章のレイヤード構成がここで効く
restRouter と Connect の routes が、同じ service 層(業務ロジック)を呼ぶようにしておけば、通信方式の違いは「入口の変換」だけになります。エラーも service 層の NotFoundError を、REST では 404、Connect では Code.NotFound に変換する薄い関数を1つずつ用意すれば済みます。
理解度チェック
Q1. 生成された todo_pb.ts を直接編集してはいけないのはなぜ?
次に buf generate したときに上書きされて消えるから。変更は必ず .proto に対して行い、再生成します。
Q2. Connect のハンドラーでエラーを返すには?
throw new ConnectError("メッセージ", Code.NotFound) のように ConnectError を投げます。それ以外の例外は Code.Internal として扱われ、詳細はクライアントに漏れません。
Q3. Node のクライアントを Connect プロトコルから gRPC プロトコルに変えるには、どこを変更する?
トランスポートだけです(createConnectTransport → createGrpcTransport)。createClient 以降のコードは変わりません。
09 実務への導入手順
「Connect 良さそうですね」から「本番で安定運用している」までの道のりです。技術そのものより、チームで .proto をどう管理し、どう変更していくかが成否を分けます。
この章のゴール
- .proto と生成コードのリポジトリ構成を設計できる
- CI で lint と互換性チェックを自動化できる
- 既存の REST から段階的に移行する計画を立てられる
リポジトリ構成
フロントエンドとバックエンドが同じリポジトリにある(モノレポ)場合の典型的な構成です。
my-app/
├── proto/ ← 契約書。ここがすべての出発点
│ └── todo/v1/todo.proto
├── buf.yaml ← lint / breaking のルール
├── buf.gen.yaml ← 生成の設定
├── packages/
│ └── api-gen/ ← 生成コード(フロントとバックで共有)
│ └── src/gen/todo/v1/todo_pb.ts
├── apps/
│ ├── server/ ← Connect サーバー(service 層・repository 層)
│ └── web/ ← React など(connect-web で呼ぶ)
└── .github/workflows/proto.yml ← CI
| 論点 | 選択肢と判断基準 |
|---|---|
| 生成コードをコミットする? | コミットする:レビューで差分が見える、ビルドに buf が不要。しない:差分がノイズにならない、生成忘れが起きない(CI で生成)。小〜中規模ならコミットし、CI で「再生成して差分がないこと」を確認する方式が扱いやすいです。 |
| リポジトリが分かれている | .proto 専用リポジトリを作るか、Buf Schema Registry(BSR)に公開して各リポジトリが npm パッケージとして取り込む。 |
| パッケージ名とバージョン |
package todo.v1; のようにディレクトリとパッケージにバージョンを入れる。壊す変更は v2 を新設して並行運用する。 |
CI で契約を守る
人間のレビューだけで互換性の破壊を防ぐのは不可能です。機械にチェックさせましょう。
name: proto
on: pull_request
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci
- run: npx buf lint
- run: npx buf format --diff --exit-code
# main ブランチの .proto と比べて、互換性を壊す変更があれば失敗させる
- run: git fetch origin main:main
- run: npx buf breaking --against '.git#branch=main'
# 生成コードが最新か(コミットしている場合)
- run: npx buf generate && git diff --exit-code
buf breaking は、例えば次のような変更を検出して PR を失敗させます。
proto/todo/v1/todo.proto:7:3: Field "3" with name "done" on message "Todo" changed type from "bool" to "string".
proto/todo/v1/todo.proto:5:1: Previously present field "4" with name "due_date" on message "Todo" was deleted.
入力検証をスキーマに書く:protovalidate
02章では zod で入力をチェックしました。Connect では、検証ルールを .proto に書いてサーバーでもクライアントでも同じルールを使う protovalidate が使えます。
import "buf/validate/validate.proto";
message CreateTodoRequest {
string title = 1 [(buf.validate.field).string = { min_len: 1, max_len: 100 }];
}
検証ルールが契約書に載るので、「フロントは100文字まで許可しているのにサーバーは50文字で弾く」といった食い違いがなくなります。
REST からの段階的な移行
「来月から全部 Connect にします」は失敗します。既存を止めずに少しずつ移すのが実務の鉄則です。
-
小さく試す(PoC)
影響の小さい社内向け機能を1つ選び、.proto 定義・生成・サーバー・クライアント・CI までを一通り作る。ここで「生成コードの置き場所」「エラーの変換方法」などの決め事を洗い出す。 -
規約を文書化する
.proto の命名規則、エラーコードの使い分け(どの業務エラーをどの Code にするか)、ページングの書き方、タイムアウトの既定値などを README にまとめる。buf lintで守れるものは機械に任せる。 -
新規機能は Connect で作る
既存 REST は触らず、新しいエンドポイントだけ Connect にする。08章の Express 同居構成がここで活きる。 -
既存を置き換える
利用が多いものから順に、同じ service 層を呼ぶ Connect 版を作り、クライアントを切り替える。旧 REST はアクセスログで利用がゼロになったのを確認してから削除する。 -
サービス間通信にも広げる
バックエンド同士は gRPC プロトコル(HTTP/2)に切り替えて効率化する。クライアントはトランスポートを差し替えるだけ。
外部公開APIは REST のままでよい
取引先や一般開発者に公開する API は、利用者が protobuf の道具を持っているとは限りません。REST + OpenAPI のほうが圧倒的に使われやすいです。Connect は JSON で叩けるとはいえ、「RESTらしい URL やキャッシュ」を期待する外部利用者には合わないこともあります。社内(自社フロント・自社サービス間)は Connect、外部公開は REST という使い分けは実務でよく見られる構成です。
本番運用のチェックリスト
-
全クライアント呼び出しに
timeoutMs(デッドライン)を設定した - ログ・認証・メトリクスをインターセプターに集約した
- 業務エラーを Connect の Code に変換するルールを決めて文書化した
- ブラウザから呼ぶ場合の CORS 設定をした(Connect のヘッダーを許可)
-
CI に
buf lintとbuf breakingを入れた - gRPC プロトコルを使う経路では L7 ロードバランサーを使っている
- ストリーミングの切断時に後片付けされることを確認した
- ヘルスチェック用のエンドポイントを用意した
理解度チェック
Q1. buf breaking を CI に入れる目的は?
既存クライアントを壊す .proto の変更(フィールド番号の変更・削除、型変更など)を、マージ前に機械的に検出するため。人間のレビューでは見落とすからです。
Q2. REST から Connect へ移行するとき、なぜ「新規機能から」始めるのが良い?
既存の動いている機能を壊すリスクがなく、チームが Connect の運用に慣れる時間を確保できるから。決め事(規約)も実際の開発を通じて固まります。
次回(最終回)は、3方式の比較と技術選定、そして ミドルエンジニアへのステップアップ に必要な視点をまとめます。