Claude CodeでgRPC開発: Protocol Buffersから運用まで実践ガイド
Claude CodeでgRPC開発を進める手順を、Node実装、期限、認証、監視、失敗例まで解説。
gRPC開発をClaude Codeに任せる前に決めること
gRPCは、離れたサービスの関数を呼び出すRPC(Remote Procedure Call)の仕組みです。RESTのようにURLとJSONを中心に考えるのではなく、.protoファイルでサービス名、メソッド、リクエスト、レスポンスを先に決めます。Protocol Buffersは、その契約を表すスキーマ言語であり、バイナリ形式で効率よくデータを送る仕組みでもあります。
Claude CodeでgRPCを扱うときのコツは、いきなり「gRPCサーバーを作って」と頼まないことです。まず契約、期限、エラー、ストリーミング、認証、監視、スキーマ変更方針を小さく分けて渡します。gRPCは型が強いぶん、最初の契約が曖昧だと、生成コード、サーバー、クライアント、テストが一斉にずれます。
公式情報は作業中に必ず確認してください。基礎はgRPC IntroductionとCore concepts、Node実装はgRPC Node basics、期限はDeadlines、エラーはStatus Codes、認証はAuthentication、監視はOpenTelemetry Metrics、スキーマ変更はProtocol Buffers proto3 guideを基準にします。
関連する全体設計はClaude Codeで本番API開発、サービス分割はClaude Codeマイクロサービス設計、契約変更はAPIバージョニング戦略、検証の広げ方はテスト戦略ガイドも合わせて読むと実務に落とし込みやすいです。
使いどころを3つ以上に絞る
gRPCは速いから使う、という説明だけでは設計判断になりません。Claude Codeに依頼する前に、どの通信をgRPCにするのかを具体化します。
| ユースケース | gRPCが向く理由 | Claude Codeに頼む作業 |
|---|---|---|
| 注文、在庫、請求など社内サービス間通信 | 型付き契約で変更漏れを見つけやすい | .proto、サーバー、クライアント、ステータスコード表を作る |
| 大量データのエクスポート | サーバーストリーミングで分割送信できる | returns (stream Item)の設計と途中失敗時の処理を書く |
| Go、Node、Pythonが混ざるチーム | 同じ.protoから複数言語の実装をそろえやすい | 言語ごとの生成コマンドとCI確認をまとめる |
| AIエージェントや内部ツール連携 | 契約が明示され、壊れた呼び出しを発見しやすい | サンプルクライアント、認証メタデータ、期限を実装する |
Masaの失敗談として、最初にREST風の「何でも1つのSearchメソッド」に寄せすぎたことがあります。短期的には実装が速く見えましたが、レスポンスが巨大化し、検索条件の互換性も曖昧になりました。Claude Codeに直させる段階では、まず.protoを読み、読み取り系、作成系、ストリーミング系に分けるレビューから始めたほうが差分が小さく済みました。
コピペで動く最小gRPCサンプル
ここではprotocを使わず、Node.jsの@grpc/proto-loaderで.protoを実行時に読み込む構成にします。GoやJavaの本番導入では静的生成もよく使いますが、初心者がClaude Codeと契約を確認する最初の検証には、この構成が軽いです。
作業ディレクトリを作り、依存関係を入れます。
mkdir claude-grpc-demo
cd claude-grpc-demo
npm init -y
npm install @grpc/grpc-js @grpc/proto-loader
mkdir proto
package.jsonは次の形にします。
{
"type": "commonjs",
"scripts": {
"server": "node server.js",
"client": "node client.js"
},
"dependencies": {
"@grpc/grpc-js": "latest",
"@grpc/proto-loader": "latest"
}
}
proto/task.protoを作ります。TaskServiceは作成、単体取得、一覧のサーバーストリーミングを持ちます。
syntax = "proto3";
package tasks.v1;
service TaskService {
rpc CreateTask(CreateTaskRequest) returns (Task);
rpc GetTask(GetTaskRequest) returns (Task);
rpc ListTasks(ListTasksRequest) returns (stream Task);
}
message Task {
string id = 1;
string title = 2;
string status = 3;
int64 created_at_unix = 4;
}
message CreateTaskRequest {
string title = 1;
}
message GetTaskRequest {
string id = 1;
}
message ListTasksRequest {
int32 limit = 1;
}
server.jsです。ローカル検証用なのでcreateInsecure()を使います。本番では後述のTLSに差し替えてください。
const path = require("node:path");
const { randomUUID } = require("node:crypto");
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const PROTO_PATH = path.join(__dirname, "proto", "task.proto");
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: false,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const taskProto = grpc.loadPackageDefinition(packageDefinition).tasks.v1;
const token = process.env.DEMO_TOKEN || "dev-token";
const tasks = new Map();
function grpcError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
function assertAuthenticated(call) {
const value = call.metadata.get("authorization")[0];
if (value !== `Bearer ${token}`) {
throw grpcError(grpc.status.UNAUTHENTICATED, "UNAUTHENTICATED");
}
}
function createTask(call, callback) {
try {
assertAuthenticated(call);
const title = String(call.request.title || "").trim();
if (!title) {
return callback(grpcError(grpc.status.INVALID_ARGUMENT, "INVALID_ARGUMENT: title"));
}
const task = {
id: randomUUID(),
title,
status: "OPEN",
createdAtUnix: String(Math.floor(Date.now() / 1000)),
};
tasks.set(task.id, task);
callback(null, task);
} catch (error) {
callback(error);
}
}
function getTask(call, callback) {
try {
assertAuthenticated(call);
const task = tasks.get(call.request.id);
if (!task) {
return callback(grpcError(grpc.status.NOT_FOUND, "NOT_FOUND: task"));
}
callback(null, task);
} catch (error) {
callback(error);
}
}
function listTasks(call) {
try {
assertAuthenticated(call);
const limit = Math.min(Math.max(Number(call.request.limit) || 10, 1), 100);
for (const task of Array.from(tasks.values()).slice(0, limit)) {
call.write(task);
}
call.end();
} catch (error) {
call.destroy(error);
}
}
const server = new grpc.Server();
server.addService(taskProto.TaskService.service, { createTask, getTask, listTasks });
server.bindAsync("127.0.0.1:50051", grpc.ServerCredentials.createInsecure(), (error, port) => {
if (error) throw error;
console.log(`TaskService listening on ${port}`);
});
client.jsです。各RPCに1秒の期限を付け、認証メタデータを送ります。
const path = require("node:path");
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const PROTO_PATH = path.join(__dirname, "proto", "task.proto");
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: false,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const taskProto = grpc.loadPackageDefinition(packageDefinition).tasks.v1;
const client = new taskProto.TaskService("127.0.0.1:50051", grpc.credentials.createInsecure());
const metadata = new grpc.Metadata();
metadata.set("authorization", `Bearer ${process.env.DEMO_TOKEN || "dev-token"}`);
function deadline(ms) {
return new Date(Date.now() + ms);
}
function createTask(title) {
return new Promise((resolve, reject) => {
client.createTask({ title }, metadata, { deadline: deadline(1000) }, (error, task) => {
if (error) return reject(error);
resolve(task);
});
});
}
function getTask(id) {
return new Promise((resolve, reject) => {
client.getTask({ id }, metadata, { deadline: deadline(1000) }, (error, task) => {
if (error) return reject(error);
resolve(task);
});
});
}
function listTasks(limit) {
return new Promise((resolve, reject) => {
const rows = [];
const stream = client.listTasks({ limit }, metadata, { deadline: deadline(1000) });
stream.on("data", (task) => rows.push(task));
stream.on("error", reject);
stream.on("end", () => resolve(rows));
});
}
async function main() {
const created = await createTask("Claude Code gRPC");
const fetched = await getTask(created.id);
const rows = await listTasks(10);
console.log(JSON.stringify({ created, fetched, streamed: rows.length }, null, 2));
client.close();
}
main().catch((error) => {
console.error(error.code, error.details || error.message);
client.close();
process.exitCode = 1;
});
実行はターミナルを2つ使います。
npm run server
別ターミナルで:
npm run client
成功すると、作成したタスク、取得したタスク、ストリーミングで受け取った件数がJSONで表示されます。Claude Codeにこのサンプルを拡張させるときは、「.protoを先に変更し、サーバーとクライアントを追従させ、最後にnpm run clientの結果を貼る」までを完了条件にしてください。
スキーマ進化で壊さない
Protocol Buffersで最も危ないのは、フィールド番号の再利用です。公式ガイドでも、利用中のフィールド番号はワイヤ形式の識別子なので変更してはいけない、削除した番号はreservedにする、という考え方が示されています。
たとえば担当者メールを追加するなら、新しい番号を足します。古いowner_emailを消したなら、同じ番号や名前を再利用しないよう予約します。
message Task {
string id = 1;
string title = 2;
string status = 3;
int64 created_at_unix = 4;
optional string assignee_email = 5;
reserved 6, 7;
reserved "owner_email";
}
Claude Codeへの指示では、「既存番号を変更しない」「削除した番号と名前はreservedにする」「enumの0番はUNSPECIFIEDにする」「JSON保存に使っている場合はProtoJSONの互換性も確認する」と明記します。ここを曖昧にすると、古いクライアントが新しいメッセージを読んだときにデータが落ちたり、将来の開発者が同じ番号を別用途に使ったりします。
期限、ストリーミング、認証、監視を最初から入れる
gRPCの期限は、クライアントが「いつまで待つか」を決める仕組みです。公式ドキュメントでは、既定では期限が設定されず、クライアントがずっと待ち続ける可能性があるため、現実的な期限を明示することが推奨されています。サンプルのdeadline(1000)は小さなローカル検証用です。本番ではP95の処理時間、ネットワーク遅延、リトライ方針を見て決めます。
ストリーミングは、大量データや進捗通知に便利です。ただし、全件を配列にためてからcall.writeするなら意味が薄く、メモリを圧迫します。サーバーストリーミングでは、途中でクライアントが切断したときの終了処理、ログ、再開方法を決めます。クライアントストリーミングや双方向ストリーミングはさらに強力ですが、順序、キャンセル、再接続が難しくなるため、最初から採用しすぎないほうが安全です。
認証は、ローカル以外でcreateInsecure()のままにしないことが最低ラインです。gRPCはSSL/TLSを組み込みで扱えます。Nodeならサーバー側はgrpc.ServerCredentials.createSsl(...)、クライアント側はgrpc.credentials.createSsl(rootCert)に置き換え、トークンはTLS上のメタデータとして送ります。メタデータだけでBearerトークンを送っても、平文通信なら守れていません。
監視は「ログを出す」だけでは足りません。OpenTelemetryのgRPCメトリクスでは、メソッド名、ステータス、時間、送受信サイズなどを見られます。最低限、grpc.method、grpc.status、処理時間、期限切れ、キャンセル、UNAVAILABLEをダッシュボード化してください。Claude Codeには「成功ログ」よりも「失敗したRPCを調査できるログ」を作らせるほうが価値があります。
具体的な落とし穴
公開前レビューでは、次の失敗例を疑ってください。
| 失敗例 | 起きること | 対策 |
|---|---|---|
| 期限を設定しない | 障害時に呼び出し元が待ち続ける | 全クライアントに現実的なdeadlineを入れる |
Create系を安易にリトライする | 二重作成や二重課金が起きる | 冪等キーを入れるか、リトライ対象を読み取り系に限定する |
| フィールド番号を再利用する | 古いデータが別の意味で解釈される | 削除番号をreservedにする |
| ストリームをメモリにためる | 大量データでサーバーが落ちる | 小さく読み、小さく書き、キャンセルを処理する |
| TLSなしでトークンを送る | 認証情報が漏れる | 本番はTLS必須、証明書更新も運用に入れる |
ステータスコードを全部UNKNOWNにする | クライアントが復旧判断できない | INVALID_ARGUMENT、NOT_FOUND、UNAUTHENTICATED、DEADLINE_EXCEEDEDを使い分ける |
Claude Codeは動くコードを速く出せますが、失敗時の設計は省略しがちです。レビュー依頼では「正常系だけでなく、期限切れ、認証失敗、存在しないID、ストリーム途中切断、サーバー停止時のクライアント表示を確認して」と具体的に書きます。
Claude Codeへの依頼テンプレート
最初の依頼は、次のように狭くします。
このリポジトリでgRPCのTaskServiceを実装してください。
制約:
- 先にproto/task.protoを作る
- Node.js + @grpc/grpc-js + @grpc/proto-loaderを使う
- CreateTask/GetTask/ListTasks(server streaming)を実装する
- 全RPCにdeadline付きクライアント例を入れる
- authorization metadataを検証する
- 本番ではTLSに差し替える注意をREADMEに書く
- npm run clientで動作確認し、出力を報告する
触ってよい範囲:
- proto/task.proto
- server.js
- client.js
- package.json
追加レビューでは、次の観点を投げます。
gRPCの公開前レビューをしてください。
重点:
- Protocol Buffersのフィールド番号再利用がないか
- deadline/cancellationの扱いがあるか
- status codeが適切か
- streamingでメモリをためていないか
- TLSなしの本番利用になっていないか
- OpenTelemetryでmethod/status/durationを追えるか
Findings firstで、重大度順に指摘してください。
この形にすると、Claude Codeが「実装しました」だけで終わりにくくなります。契約、実装、検証、運用の順番を守らせるのが、gRPC開発では特に重要です。
収益導線と次の一歩
gRPCの記事は単なる技術メモで終わらせると、検索流入はあっても収益につながりにくいです。読者はたいてい「社内サービスの契約を壊したくない」「Claude Codeにどこまで任せていいか分からない」「監視や認証を後回しにしてよいか不安」という状態で来ています。
個人で試すなら無料チートシートで日常コマンドとレビュー観点を固め、チーム導入ならClaude Code研修・導入相談で.proto運用、CI、権限、レビューゲートをリポジトリに合わせて設計するのが現実的です。テンプレートや教材を整えたい場合はプロダクト一覧から、API開発、マイクロサービス、検証レシートの型を組み合わせてください。
この記事で紹介した内容を実際に試した結果、Node v24.14.1とnpm 11.11.0の一時ディレクトリでnpm install、node server.js、node client.jsまで通り、created、fetched、streamed: 1を確認できました。一方、この環境にはgo、protoc、protoc-gen-goがなかったため、Goの静的生成例は載せず、実行確認できたNodeの動的読み込み例に絞りました。公開前レビューでは、公式リンク、内部リンク、updatedDate、description文字数、コードフェンス、認証/TLSの注意、期限、ストリーミング、監視、スキーマ進化の説明がそろっていることを確認してください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。