gRPC solves specific problems better than REST: strongly-typed contracts via Protocol Buffers, bidirectional streaming, and high-performance binary serialization for inter-service communication. Claude Code handles the protobuf-to-code generation workflow, generates idiomatic server implementations, and understands the patterns that make gRPC services maintainable.
This guide covers gRPC with Claude Code: protobuf design, server implementation, streaming patterns, and client generation.
Protocol Buffer Design
CLAUDE.md for gRPC Projects
## gRPC Service
- Language: TypeScript (Node.js server) + TypeScript (browser client via gRPC-web)
- Protobuf: proto3 syntax, buf CLI for linting and breaking change detection
- Code generation: buf generate (replaces protoc)
- Testing: grpc-mock for integration tests
## Protobuf conventions
- Package: {company}.{service}.v1
- Service names: PascalCase noun (OrderService, not OrderManager)
- RPC methods: PascalCase verb+noun (CreateOrder, ListOrders, StreamOrderStatus)
- Message names: Singular for request/response (CreateOrderRequest not CreateOrderRequests)
- Field names: snake_case
- Always include field numbers in sequence — never reuse deleted field numbers
- Use well-known types: google.protobuf.Timestamp (not string for dates), google.protobuf.FieldMask
## Versioning
- /v1/ in package path — when breaking changes needed, create v2
- Non-breaking changes ok in v1: adding optional fields, adding RPCs
- Breaking changes that require v2: removing fields, changing field types
Service Definition
// proto/orders/v1/orders.proto
syntax = "proto3";
package company.orders.v1;
import "google/protobuf/timestamp.proto";
import "google/protobuf/field_mask.proto";
option go_package = "github.com/mycompany/services/orders/v1;ordersv1";
option java_package = "com.mycompany.orders.v1";
service OrderService {
// Unary RPC
rpc CreateOrder(CreateOrderRequest) returns (Order);
rpc GetOrder(GetOrderRequest) returns (Order);
rpc UpdateOrder(UpdateOrderRequest) returns (Order);
rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse);
// Server-streaming: real-time order status updates
rpc StreamOrderStatus(StreamOrderStatusRequest) returns (stream OrderStatusUpdate);
// Client-streaming: bulk order import
rpc ImportOrders(stream ImportOrderRequest) returns (ImportOrdersResponse);
// Bidirectional streaming: live order processing dashboard
rpc MonitorOrders(stream OrderFilter) returns (stream Order);
}
message Order {
string id = 1;
string user_id = 2;
repeated OrderItem items = 3;
int64 total_cents = 4; // Amount in cents — avoid floats for money
OrderStatus status = 5;
google.protobuf.Timestamp created_at = 6;
google.protobuf.Timestamp updated_at = 7;
ShippingAddress shipping_address = 8;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
int64 price_cents = 3;
string product_name = 4;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0; // Required: proto3 default must be 0
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_CONFIRMED = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_DELIVERED = 4;
ORDER_STATUS_CANCELLED = 5;
}
message CreateOrderRequest {
string user_id = 1;
repeated OrderItem items = 2;
ShippingAddress shipping_address = 3;
string idempotency_key = 4; // Prevent duplicate submission
}
message UpdateOrderRequest {
string id = 1;
Order order = 2;
google.protobuf.FieldMask update_mask = 3; // Which fields to update
}
message StreamOrderStatusRequest {
string order_id = 1;
}
message OrderStatusUpdate {
string order_id = 1;
OrderStatus status = 2;
string message = 3; // Human-readable status description
google.protobuf.Timestamp timestamp = 4;
}
TypeScript Server Implementation
Implement the OrderService server in TypeScript.
Include proper error handling with gRPC status codes.
// src/services/order-service.ts
import {
ServerUnaryCall,
sendUnaryData,
ServerWritableStream,
ServiceError,
status,
} from '@grpc/grpc-js';
import {
OrderServiceServer,
CreateOrderRequest,
GetOrderRequest,
Order,
StreamOrderStatusRequest,
OrderStatusUpdate,
} from '../generated/orders/v1/orders_grpc_pb';
import { OrderRepository } from '../repositories/order-repository';
import { OrderStatusBus } from '../lib/order-status-bus';
export class OrderServiceImpl implements OrderServiceServer {
[method: string]: unknown;
constructor(
private readonly orders: OrderRepository,
private readonly statusBus: OrderStatusBus,
) {}
async createOrder(
call: ServerUnaryCall<CreateOrderRequest, Order>,
callback: sendUnaryData<Order>,
): Promise<void> {
try {
const request = call.request;
// Validate
if (!request.getUserId()) {
callback({
code: status.INVALID_ARGUMENT,
message: 'user_id is required',
} as ServiceError);
return;
}
if (request.getItemsList().length === 0) {
callback({
code: status.INVALID_ARGUMENT,
message: 'Order must contain at least one item',
} as ServiceError);
return;
}
// Idempotency check
const idempotencyKey = request.getIdempotencyKey();
if (idempotencyKey) {
const existing = await this.orders.findByIdempotencyKey(idempotencyKey);
if (existing) {
callback(null, existing); // Return existing order
return;
}
}
const order = await this.orders.create({
userId: request.getUserId(),
items: request.getItemsList().map(item => ({
productId: item.getProductId(),
quantity: item.getQuantity(),
priceCents: item.getPriceCents(),
productName: item.getProductName(),
})),
shippingAddress: request.getShippingAddress()?.toObject(),
idempotencyKey,
});
callback(null, this.orderToProto(order));
} catch (error) {
console.error('CreateOrder error:', error);
callback({
code: status.INTERNAL,
message: 'Internal server error',
} as ServiceError);
}
}
// Server-streaming RPC
streamOrderStatus(
call: ServerWritableStream<StreamOrderStatusRequest, OrderStatusUpdate>,
): void {
const orderId = call.request.getOrderId();
// Subscribe to order status updates
const unsubscribe = this.statusBus.subscribe(orderId, (update) => {
const statusUpdate = new OrderStatusUpdate();
statusUpdate.setOrderId(update.orderId);
statusUpdate.setStatus(update.status);
statusUpdate.setMessage(update.message);
// write() returns false when the client can't keep up (backpressure)
if (!call.write(statusUpdate)) {
call.once('drain', () => {/* Continue after buffer drains */});
}
});
// Clean up subscription when client disconnects
call.on('cancelled', () => unsubscribe());
call.on('error', () => unsubscribe());
// Complete stream when order reaches final state
this.statusBus.onFinalState(orderId, () => {
unsubscribe();
call.end();
});
}
}
Interceptors
Add authentication and request logging to all gRPC calls.
// src/interceptors/auth-interceptor.ts
import {
ServerInterceptingCall,
InterceptingCall,
status,
Metadata,
} from '@grpc/grpc-js';
import { verifyJWT } from '../lib/jwt';
export function authInterceptor(
options: object,
nextCall: (options: object) => InterceptingCall,
) {
return new ServerInterceptingCall(nextCall(options), {
start: function(metadata: Metadata, listener, next) {
const token = metadata.get('authorization')[0] as string;
// Skip auth for health check
if (options.method_definition?.path === '/grpc.health.v1.Health/Check') {
next(metadata, listener);
return;
}
if (!token?.startsWith('Bearer ')) {
listener.onReceiveStatus({
code: status.UNAUTHENTICATED,
details: 'Missing authorization token',
});
return;
}
const jwt = token.slice(7);
const user = verifyJWT(jwt);
if (!user) {
listener.onReceiveStatus({
code: status.UNAUTHENTICATED,
details: 'Invalid or expired token',
});
return;
}
// Add user to metadata for use in handlers
metadata.set('x-user-id', user.id);
metadata.set('x-user-roles', JSON.stringify(user.roles));
next(metadata, listener);
},
});
}
gRPC-Web for Browser Clients
Generate a TypeScript client that works in the browser.
Our frontend needs to call the OrderService.
// Generated client usage (via buf generate with connect-web plugin)
import { createConnectTransport } from '@connectrpc/connect-web';
import { createClient } from '@connectrpc/connect';
import { OrderService } from './generated/orders/v1/orders_connect';
const transport = createConnectTransport({
baseUrl: 'https://api.myapp.com',
});
const client = createClient(OrderService, transport);
// Unary call
const order = await client.createOrder({
userId: 'user-123',
items: [{ productId: 'prod-1', quantity: 2, priceCents: BigInt(1999) }],
});
// Server-streaming: real-time order updates
async function trackOrder(orderId: string) {
for await (const update of client.streamOrderStatus({ orderId })) {
console.log('Status update:', update.status, update.message);
if (update.status === OrderStatus.ORDER_STATUS_DELIVERED) {
break; // Stop listening after delivery
}
}
}
For deploying gRPC services in Kubernetes with Envoy as a gateway, see the Kubernetes guide. For REST APIs where gRPC isn’t the right fit, see the API design guide. For microservices architecture decisions — when to use gRPC vs REST vs message queues — see the microservices guide. The Claude Skills 360 bundle includes gRPC skill sets for Protobuf design and server implementation. Start with the free tier to generate gRPC service definitions.