| name | grpc |
| description | gRPC framework best practices and implementation |
| license | MIT |
| compatibility | opencode |
| metadata | {"audience":"developers","category":"api-design"} |
What I do
- Define Protocol Buffer schemas
- Implement gRPC services
- Handle streaming
- Implement authentication
- Handle errors properly
- Manage versioning
- Test gRPC services
- Scale gRPC deployments
When to use me
When implementing gRPC services or working with Protocol Buffers.
Protocol Buffer Definition
syntax = "proto3";
package user.v1;
option go_package = "github.com/company/api/user/v1";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
// Service definition
service UserService {
// Unary RPC
rpc GetUser(GetUserRequest) returns (User);
// Server streaming
rpc ListUsers(ListUsersRequest) returns (stream User);
// Client streaming
rpc CreateUsers(stream CreateUserRequest) returns (CreateUsersResponse);
// Bidirectional streaming
rpc StreamUserUpdates(stream UserUpdateRequest) returns (stream User);
// Health check
rpc HealthCheck(google.protobuf.Empty) returns (HealthCheckResponse);
}
// Message types
message GetUserRequest {
string user_id = 1;
}
message User {
string id = 1;
string email = 2;
string name = 3;
UserStatus status = 4;
google.protobuf.Timestamp created_at = 5;
google.protobuf.Timestamp updated_at = 6;
}
enum UserStatus {
USER_STATUS_UNSPECIFIED = 0;
USER_STATUS_ACTIVE = 1;
USER_STATUS_INACTIVE = 2;
USER_STATUS_SUSPENDED = 3;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
UserStatus status_filter = 3;
}
message CreateUserRequest {
string email = 1;
string name = 2;
string password = 3;
}
message CreateUsersResponse {
repeated User users = 1;
int32 failed_count = 2;
}
message UserUpdateRequest {
string user_id = 1;
string name = 2;
}
message HealthCheckResponse {
bool healthy = 1;
string version = 2;
}
Go gRPC Server
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
pb "github.com/company/api/user/v1"
)
type UserServer struct {
pb.UnimplementedUserServiceServer
userStore UserStore
}
type UserStore interface {
GetUser(ctx context.Context, id string) (*pb.User, error)
ListUsers(ctx context.Context, filter *pb.UserStatus, limit int) ([]*pb.User, error)
CreateUser(ctx context.Context, user *pb.User) error
}
func NewUserServer(store UserStore) *UserServer {
return &UserServer{userStore: store}
}
func (s *UserServer) GetUser(
ctx context.Context,
req *pb.GetUserRequest,
) (*pb.User, error) {
md, ok := metadata.FromIncomingContext(ctx)
if ok {
log.Printf("GetUser request: user_id=%s, from=%v", req.UserId, md.Get("x-forwarded-for"))
}
if req.UserId == "" {
return nil, status.Error(codes.InvalidArgument, "user_id is required")
}
user, err := s.userStore.GetUser(ctx, req.UserId)
if err != {
, status.Errorf(codes.NotFound, , err)
}
user,
}
ListUsers(
req *pb.ListUsersRequest,
stream pb.UserService_ListUsersServer,
) {
ctx := stream.Context()
pageSize := (req.PageSize)
pageSize <= {
pageSize =
}
pageSize > {
pageSize =
}
users, err := s.userStore.ListUsers(ctx, req.StatusFilter, pageSize)
err != {
status.Errorf(codes.Internal, , err)
}
_, user := users {
err := stream.Send(user); err != {
status.Errorf(codes.Internal, , err)
}
}
}
CreateUsers(
stream pb.UserService_CreateUsersServer,
) {
ctx := stream.Context()
users []*pb.User
failedCount
{
req, err := stream.Recv()
err != {
err.Error() == {
}
status.Errorf(codes.Internal, , err)
}
user := &pb.User{
Email: req.Email,
Name: req.Name,
Status: pb.UserStatus_USER_STATUS_ACTIVE,
CreatedAt: timestamppb.Now(),
}
err := s.userStore.CreateUser(ctx, user); err != {
failedCount++
log.Printf(, err)
}
users = (users, user)
}
stream.SendAndClose(&pb.CreateUsersResponse{
Users: users,
FailedCount: failedCount,
})
}
StreamUserUpdates(
stream pb.UserService_StreamUserUpdatesServer,
) {
ctx := stream.Context()
{
{
<-ctx.Done():
ctx.Err()
:
req, err := stream.Recv()
err != {
err
}
user, err := s.userStore.GetUser(ctx, req.UserId)
err != {
}
err := stream.Send(user); err != {
err
}
}
}
}
{
}
gRPC Client
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
pb "github.com/company/api/user/v1"
)
type UserClient struct {
conn *grpc.ClientConn
client pb.UserServiceClient
}
func NewUserClient(addr string) (*UserClient, error) {
conn, err := grpc.Dial(
addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(loggingInterceptor),
grpc.WithStreamInterceptor(streamLoggingInterceptor),
)
if err != nil {
return nil, err
}
return &UserClient{
conn: conn,
client: pb.NewUserServiceClient(conn),
}, nil
}
func (c *UserClient) GetUser(ctx context.Context, userId string) (*pb.User, error) {
ctx = metadata.AppendToOutgoingContext(ctx, "x-request-id", "req-123")
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
return c.client.GetUser(ctx, &pb.GetUserRequest{
UserId: userId,
})
}
func (c *UserClient) ListActiveUsers(ctx context.Context) ([]*pb.User, error) {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
stream, err := c.client.ListUsers(ctx, &pb.ListUsersRequest{
PageSize: 100,
StatusFilter: pb.UserStatus_USER_STATUS_ACTIVE,
})
err != {
, err
}
users []*pb.User
{
user, err := stream.Recv()
err != {
err.Error() == {
}
, err
}
users = (users, user)
}
users,
}
Close() {
c.conn.Close()
}
{
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
log.Printf(,
method, time.Since(start), err)
err
}
{
start := time.Now()
err := stream.RecvMsg()
log.Printf(,
method, time.Since(start), err)
err
}
Error Handling
package status
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
ErrCodeUserNotFound = "USER_NOT_FOUND"
ErrCodeInvalidInput = "INVALID_INPUT"
ErrCodeUnauthorized = "UNAUTHORIZED"
)
func UserNotFound(id string) error {
return status.Errorf(codes.NotFound, "user not found: %s", id)
}
func InvalidInput(field, reason string) error {
return status.Errorf(codes.InvalidArgument, "invalid %s: %s", field, reason)
}
func Unauthorized(reason string) error {
return status.Errorf(codes.Unauthenticated, "unauthorized: %s", reason)
}
gRPC Best Practices
1. Use Protocol Buffers for schema
- Define contracts explicitly
- Enable backwards compatibility
2. Handle errors properly
- Use gRPC status codes
- Include error details
3. Implement interceptors
- Logging, auth, metrics
4. Use streaming appropriately
- Server streaming for lists
- Bidirectional for real-time
5. Set timeouts
- Always use context with timeout
6. Enable reflection
- For debugging and tools
7. Version your APIs
- Include version in package name
- Support multiple versions
8. Use proper authentication
- Token-based or mTLS
9. Monitor and trace
- Add interceptors for metrics
- Use OpenTelemetry
10. Test thoroughly
- Unit tests with mock stores
- Integration tests