Claude Code for Protocol Buffers and gRPC: Schema Design and Service Implementation — Claude Skills 360 Blog
Blog / Backend / Claude Code for Protocol Buffers and gRPC: Schema Design and Service Implementation
Backend

Claude Code for Protocol Buffers and gRPC: Schema Design and Service Implementation

Published: September 15, 2026
Read time: 9 min read
By: Claude Skills 360

gRPC and Protocol Buffers give you a strongly-typed, versioned API contract that generates client and server code in any language. The schema is the contract — both sides of the API know exactly what fields exist, what types they are, and what’s optional. Claude Code designs proto schemas correctly, implements service handlers, and understands where gRPC outperforms REST (bidirectional streaming, generated clients, tight performance requirements).

Proto Schema Design

CLAUDE.md for gRPC Projects

## gRPC/Protobuf Configuration
- protoc 25+, protoc-gen-go 1.33, protoc-gen-go-grpc 1.4
- Proto files: proto/ directory, organized by service
- Generated code: gen/ directory (committed to repo)
- Style: Google API Design Guide — camelCase fields, snake_case in proto
- Versioning: v1/v2 packages in proto paths (proto/orders/v1/orders.proto)
- Validation: protoc-gen-validate for request validation
- Connect protocol also supported (grpc-web-compatible via connectrpc)
// proto/orders/v1/orders.proto
syntax = "proto3";

package orders.v1;

option go_package = "github.com/mycompany/proto/gen/orders/v1;ordersv1";

import "google/protobuf/timestamp.proto";
import "google/protobuf/field_mask.proto";
import "validate/validate.proto";

// Order status enum — exhaustive, with UNSPECIFIED as zero value
enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_PENDING = 1;
  ORDER_STATUS_CONFIRMED = 2;
  ORDER_STATUS_SHIPPED = 3;
  ORDER_STATUS_DELIVERED = 4;
  ORDER_STATUS_CANCELLED = 5;
}

message Order {
  string order_id = 1;           // UUID
  string customer_id = 2;        // FK to customer service
  
  repeated OrderItem items = 3;
  
  OrderStatus status = 4;
  int64 total_cents = 5;         // Avoid floats for money
  
  google.protobuf.Timestamp created_at = 6;
  google.protobuf.Timestamp updated_at = 7;
  
  // Optional: only set when shipped
  optional string tracking_number = 8;
}

message OrderItem {
  string product_id = 1;
  string product_name = 2;
  int32 quantity = 3 [(validate.rules).int32 = {gt: 0, lte: 1000}];
  int64 unit_price_cents = 4;
}

// Request/Response messages — one per RPC, never reuse
message CreateOrderRequest {
  string customer_id = 1 [(validate.rules).string = {uuid: true}];
  repeated CreateOrderItem items = 2 [(validate.rules).repeated = {min_items: 1, max_items: 50}];
  string payment_method_id = 3 [(validate.rules).string = {min_len: 1}];
}

message CreateOrderItem {
  string product_id = 1 [(validate.rules).string = {uuid: true}];
  int32 quantity = 2 [(validate.rules).int32 = {gt: 0, lte: 100}];
}

message CreateOrderResponse {
  Order order = 1;
}

message GetOrderRequest {
  string order_id = 1 [(validate.rules).string = {uuid: true}];
  // Field mask: client specifies which fields to return
  google.protobuf.FieldMask field_mask = 2;
}

message GetOrderResponse {
  Order order = 1;
}

message ListOrdersRequest {
  string customer_id = 1;
  OrderStatus status_filter = 2;
  int32 page_size = 3 [(validate.rules).int32 = {gte: 1, lte: 100}];
  string page_token = 4;
}

message ListOrdersResponse {
  repeated Order orders = 1;
  string next_page_token = 2;  // Empty = no more pages
}

// Streaming: server pushes order status updates to client
message WatchOrderRequest {
  string order_id = 1 [(validate.rules).string = {uuid: true}];
}

message OrderStatusUpdate {
  string order_id = 1;
  OrderStatus old_status = 2;
  OrderStatus new_status = 3;
  google.protobuf.Timestamp occurred_at = 4;
}

service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
  rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
  rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
  
  // Server-streaming: client subscribes, server pushes updates
  rpc WatchOrder(WatchOrderRequest) returns (stream OrderStatusUpdate);
  
  // Client-streaming: upload batch of orders at once
  rpc ImportOrders(stream CreateOrderRequest) returns (ImportOrdersResponse);
}

message ImportOrdersResponse {
  int32 imported_count = 1;
  int32 failed_count = 2;
  repeated ImportError errors = 3;
}

message ImportError {
  int32 index = 1;  // Which request failed
  string message = 2;
}

Go Service Implementation

Implement the OrderService gRPC server in Go.
Use the generated types from the proto.
// internal/grpc/orders_server.go
package grpc

import (
	"context"
	"io"
	"sync"
	
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"
	
	ordersv1 "github.com/mycompany/proto/gen/orders/v1"
	"github.com/mycompany/order-service/internal/repository"
	"github.com/mycompany/order-service/internal/service"
)

type OrderServer struct {
	ordersv1.UnimplementedOrderServiceServer // Forward-compatible: callers of unimplemented methods get UNIMPLEMENTED error
	orderRepo  *repository.OrderRepository
	orderSvc   *service.OrderService
	// Watchers: order_id → list of channels awaiting updates
	mu       sync.RWMutex
	watchers map[string][]chan *ordersv1.OrderStatusUpdate
}

func (s *OrderServer) CreateOrder(ctx context.Context, req *ordersv1.CreateOrderRequest) (*ordersv1.CreateOrderResponse, error) {
	// Validation is handled by the validate interceptor from protoc-gen-validate
	
	order, err := s.orderSvc.CreateOrder(ctx, service.CreateOrderInput{
		CustomerID:      req.CustomerId,
		Items:           convertItems(req.Items),
		PaymentMethodID: req.PaymentMethodId,
	})
	if err != nil {
		// Map domain errors to gRPC status codes
		switch err {
		case service.ErrCustomerNotFound:
			return nil, status.Errorf(codes.NotFound, "customer %s not found", req.CustomerId)
		case service.ErrInsufficientInventory:
			return nil, status.Errorf(codes.FailedPrecondition, "insufficient inventory")
		case service.ErrPaymentDeclined:
			// Include structured error details
			st := status.New(codes.FailedPrecondition, "payment declined")
			return nil, st.Err()
		default:
			return nil, status.Errorf(codes.Internal, "internal error creating order")
		}
	}
	
	return &ordersv1.CreateOrderResponse{Order: orderToProto(order)}, nil
}

// Server-streaming RPC: push status updates to client
func (s *OrderServer) WatchOrder(req *ordersv1.WatchOrderRequest, stream ordersv1.OrderService_WatchOrderServer) error {
	orderID := req.OrderId
	
	// Verify order exists and caller has access
	if _, err := s.orderRepo.Get(stream.Context(), orderID); err != nil {
		return status.Errorf(codes.NotFound, "order %s not found", orderID)
	}
	
	// Register this stream as a watcher
	ch := make(chan *ordersv1.OrderStatusUpdate, 10)
	s.mu.Lock()
	s.watchers[orderID] = append(s.watchers[orderID], ch)
	s.mu.Unlock()
	
	defer func() {
		// Deregister on stream close
		s.mu.Lock()
		watchers := s.watchers[orderID]
		for i, w := range watchers {
			if w == ch {
				s.watchers[orderID] = append(watchers[:i], watchers[i+1:]...)
				break
			}
		}
		s.mu.Unlock()
		close(ch)
	}()
	
	for {
		select {
		case update, ok := <-ch:
			if !ok {
				return nil
			}
			if err := stream.Send(update); err != nil {
				return err // Client disconnected
			}
		case <-stream.Context().Done():
			return stream.Context().Err()
		}
	}
}

// Called by order service when status changes
func (s *OrderServer) NotifyStatusChange(orderID string, oldStatus, newStatus ordersv1.OrderStatus) {
	s.mu.RLock()
	watchers := s.watchers[orderID]
	s.mu.RUnlock()
	
	update := &ordersv1.OrderStatusUpdate{
		OrderId:    orderID,
		OldStatus:  oldStatus,
		NewStatus:  newStatus,
		OccurredAt: timestamppb.Now(),
	}
	
	for _, ch := range watchers {
		select {
		case ch <- update:
		default:
			// Channel full — drop update (client is too slow)
		}
	}
}

// Client-streaming RPC: accept batch of orders
func (s *OrderServer) ImportOrders(stream ordersv1.OrderService_ImportOrdersServer) error {
	var imported, failed int32
	var errors []*ordersv1.ImportError
	var index int32
	
	for {
		req, err := stream.Recv()
		if err == io.EOF {
			break
		}
		if err != nil {
			return status.Errorf(codes.Internal, "failed to receive: %v", err)
		}
		
		_, createErr := s.orderSvc.CreateOrder(stream.Context(), service.CreateOrderInput{
			CustomerID: req.CustomerId,
			Items:      convertItems(req.Items),
		})
		
		if createErr != nil {
			failed++
			errors = append(errors, &ordersv1.ImportError{
				Index:   index,
				Message: createErr.Error(),
			})
		} else {
			imported++
		}
		index++
	}
	
	return stream.SendAndClose(&ordersv1.ImportOrdersResponse{
		ImportedCount: imported,
		FailedCount:   failed,
		Errors:        errors,
	})
}

gRPC Interceptors

Add request logging, metrics, and auth validation
to all gRPC endpoints without touching each handler.
// internal/grpc/interceptors.go

// Unary interceptor (for non-streaming RPCs)
func LoggingUnaryInterceptor(logger *zap.Logger) grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		start := time.Now()
		
		resp, err := handler(ctx, req)
		
		duration := time.Since(start)
		code := status.Code(err)
		
		logger.Info("gRPC request",
			zap.String("method", info.FullMethod),
			zap.Duration("duration", duration),
			zap.String("code", code.String()),
			zap.Error(err),
		)
		
		// Record Prometheus metrics
		grpcRequestDuration.WithLabelValues(info.FullMethod, code.String()).Observe(duration.Seconds())
		
		return resp, err
	}
}

// Auth interceptor — validates JWT from metadata
func AuthUnaryInterceptor(verifier *jwt.Verifier) grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		// Skip auth for reflection and health check
		if info.FullMethod == "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo" {
			return handler(ctx, req)
		}
		
		md, ok := metadata.FromIncomingContext(ctx)
		if !ok {
			return nil, status.Error(codes.Unauthenticated, "missing metadata")
		}
		
		authHeaders := md.Get("authorization")
		if len(authHeaders) == 0 {
			return nil, status.Error(codes.Unauthenticated, "missing authorization header")
		}
		
		token := strings.TrimPrefix(authHeaders[0], "Bearer ")
		claims, err := verifier.Verify(token)
		if err != nil {
			return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
		}
		
		// Add claims to context for handlers
		ctx = context.WithValue(ctx, ctxKeyUserID, claims.Subject)
		return handler(ctx, req)
	}
}

TypeScript gRPC Client

Generate a TypeScript client for the OrderService.
Use Connect RPC for browser compatibility.
// src/lib/ordersClient.ts
import { createClient } from '@connectrpc/connect';
import { createConnectTransport } from '@connectrpc/connect-node';
import { OrderService } from './gen/orders/v1/orders_connect';
import { OrderStatus_Value } from './gen/orders/v1/orders_pb';

const transport = createConnectTransport({
  baseUrl: process.env.ORDER_SERVICE_URL ?? 'https://orders.internal:8443',
  httpVersion: '2',
});

export const ordersClient = createClient(OrderService, transport);

// Usage
async function createOrder(customerId: string, items: OrderItem[]) {
  const response = await ordersClient.createOrder({
    customerId,
    items: items.map(i => ({ productId: i.productId, quantity: i.quantity })),
    paymentMethodId: 'pm_xxx',
  });
  
  return response.order;
}

// Server-streaming subscription
async function watchOrder(orderId: string, onUpdate: (status: string) => void) {
  const stream = ordersClient.watchOrder({ orderId });
  
  for await (const update of stream) {
    onUpdate(OrderStatus_Value[update.newStatus]);
  }
}

For the microservices architecture that gRPC services typically form part of, see the Go microservices guide. For generating TypeScript client code from OpenAPI instead of Protobuf, the OpenAPI codegen guide covers that workflow. The Claude Skills 360 bundle includes gRPC skill sets for proto design, service implementation in multiple languages, and interceptor patterns. Start with the free tier to try proto schema generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free