Claude Code gRPC Development: Protobuf, Streaming, Deadlines, Auth
Build a runnable gRPC service with Claude Code, Protobuf, deadlines, streaming, auth, and observability.
Start With The Contract, Not The Server
gRPC is a remote procedure call framework: a client calls a method on another service as if it were a local function, while the real work happens over the network. Protocol Buffers define the contract: service names, methods, request messages, response messages, and field numbers. That contract is the part Claude Code must respect before it writes server or client code.
Use the official docs as the source of truth while working: gRPC Introduction, Core concepts, Node basics, Deadlines, Status Codes, Authentication, OpenTelemetry Metrics, and the Protocol Buffers proto3 guide.
For adjacent ClaudeCodeLab material, pair this with production API development, microservices with Claude Code, API versioning strategy, and testing strategies.
Practical Use Cases
Do not choose gRPC only because it sounds fast. Choose it when a typed contract and clear failure behavior help the system.
| Use case | Why gRPC fits | What to ask Claude Code to do |
|---|---|---|
| Internal order, inventory, billing services | The contract catches drift between teams | Draft .proto, server, client, and status-code mapping |
| Large exports or feeds | Server streaming avoids one huge response | Implement returns (stream Item) plus cancellation handling |
| Mixed Go, Node, Python services | One schema can drive several language clients | Document generation commands and CI checks |
| Internal AI tools and agents | Explicit methods reduce ambiguous tool calls | Add sample clients, metadata auth, deadlines, and logs |
Masa’s practical lesson from small service work is that a single broad Search method looks convenient but becomes hard to evolve. Split read, create, and streaming workflows early, then let Claude Code update each file against that contract.
Runnable Node gRPC Example
This sample uses @grpc/proto-loader, so it does not require protoc for the first local proof. Create a directory and install the dependencies:
mkdir claude-grpc-demo
cd claude-grpc-demo
npm init -y
npm install @grpc/grpc-js @grpc/proto-loader
mkdir proto
Use this package.json:
{
"type": "commonjs",
"scripts": {
"server": "node server.js",
"client": "node client.js"
},
"dependencies": {
"@grpc/grpc-js": "latest",
"@grpc/proto-loader": "latest"
}
}
Create proto/task.proto:
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;
}
Create server.js:
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}`);
});
Create client.js:
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;
});
Run the server in one terminal:
npm run server
Run the client in another:
npm run client
Evolution, Security, And Failure Modes
Schema evolution is where thin gRPC articles usually fail. Never reuse field numbers. If a field is deleted, reserve its number and name. Add new fields with new numbers, prefer explicit presence when the client must distinguish “unset” from “empty”, and keep enum value 0 for an unspecified state.
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";
}
Deadlines are not optional discipline. The gRPC docs note that clients can otherwise wait indefinitely, so every client call should set a realistic deadline. Streaming is useful for exports and live progress, but it must handle cancellation and avoid buffering the whole result in memory.
The local sample uses createInsecure() only to stay copyable. Production traffic should use TLS with grpc.ServerCredentials.createSsl(...) on the server and grpc.credentials.createSsl(rootCert) on the client. Bearer tokens in metadata are not enough if the channel is not encrypted.
Observability should include method, status, duration, cancellation, deadline expiry, and unavailable backends. OpenTelemetry gRPC metrics are a good baseline because they expose per-call client and server measurements. For failures, avoid returning everything as UNKNOWN; use INVALID_ARGUMENT, NOT_FOUND, UNAUTHENTICATED, DEADLINE_EXCEEDED, UNAVAILABLE, and RESOURCE_EXHAUSTED intentionally. Retry only idempotent calls unless you have an idempotency key.
Prompt Claude Code With Review Criteria
Use a scoped implementation prompt:
Implement TaskService with gRPC.
Constraints:
- Create proto/task.proto first.
- Use Node.js, @grpc/grpc-js, and @grpc/proto-loader.
- Implement CreateTask, GetTask, and ListTasks as server streaming.
- Add client deadlines to every RPC.
- Validate authorization metadata.
- Mention that production must replace createInsecure with TLS.
- Run npm run client and report the output.
Editable files:
- proto/task.proto
- server.js
- client.js
- package.json
Then ask for a critical review:
Review this gRPC implementation findings first.
Check schema evolution, field-number reuse, deadlines, cancellation,
status codes, streaming memory behavior, TLS/auth, and observability.
Rank issues by severity and include exact file references.
CTA And Verification Result
If you are learning alone, start with the free Claude Code cheatsheet and adapt this sample to a throwaway service. Teams that need .proto governance, CI gates, review prompts, TLS rollout, and observability can use Claude Code training and consultation. For reusable setup material, see the ClaudeCodeLab products.
For this refresh, the sample was actually run in a temporary directory with Node v24.14.1 and npm 11.11.0: npm install, node server.js, and node client.js completed, and the client printed created, fetched, and streamed: 1. Go, protoc, and the Go protobuf plugins were not available on this machine, so the article intentionally uses the Node dynamic-loading path that could be verified here.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.