Skip to main content
โšก Calmops

gRPC API Design: High-Performance APIs

gRPC API Design: High-Performance APIs

gRPC is a high-performance RPC framework that uses HTTP/2 for transport and Protocol Buffers for serialization. This guide covers everything you need to design efficient gRPC APIs.

What is gRPC?

gRPC (Google Remote Procedure Call) is an open-source RPC framework that uses:

  • Protocol Buffers for efficient serialization
  • HTTP/2 for multiplexed connections
  • Code generation for strongly-typed clients

Why gRPC?

  • Performance: 7-10x faster than REST
  • Streaming: Built-in support for bi-directional streaming
  • Code Generation: Type-safe contracts
  • Cross-language: Clients in many languages

Protocol Buffers Basics

Defining Messages

syntax = "proto3";

package myapp;

message User {
  string id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;
  bool active = 5;
}

message Order {
  string id = 1;
  User customer = 2;
  repeated Item items = 3;
  Status status = 4;
}

message Item {
  string product_id = 1;
  string name = 2;
  int32 quantity = 3;
  double price = 4;
}

enum Status {
  STATUS_UNSPECIFIED = 0;
  STATUS_PENDING = 1;
  STATUS_COMPLETED = 2;
  STATUS_CANCELLED = 3;
}

Scalar Types

Proto Type Go Type Python Type Description
double float64 float 64-bit float
float float32 float 32-bit float
int32 int32 int Variable-length int
int64 int64 int 64-bit int
string string str UTF-8 string
bool bool bool Boolean
bytes []byte bytes Byte sequence

gRPC Service Definition

Simple RPC

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc CreateUser (CreateUserRequest) returns (User);
  rpc UpdateUser (UpdateUserRequest) returns (User);
  rpc DeleteUser (DeleteUserRequest) returns (Empty);
  rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
}

message GetUserRequest {
  string id = 1;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
  int32 age = 3;
}

message ListUsersRequest {
  int32 page_size = 1;
  string page_token = 2;
}

message ListUsersResponse {
  repeated User users = 1;
  string next_page_token = 2;
}

message Empty {}

Server Streaming

service NotificationService {
  rpc StreamNotifications (NotificationRequest) returns (stream Notification);
}

message NotificationRequest {
  string user_id = 1;
}

message Notification {
  string id = 1;
  string message = 2;
  string type = 3;
}

Client Streaming

service UploadService {
  rpc Upload (stream UploadRequest) returns (UploadResponse);
}

Bi-directional Streaming

service ChatService {
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}

Code Generation

Setup

apt-get install protobuf-compiler
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       proto/user.proto

Server Implementation (Go)

import (
    "context"
    "net"
    "google.golang.org/grpc"
    "myapp/proto/user"
)

type userService struct {
    user.UnimplementedUserServiceServer
    db *Database
}

func (s *userService) GetUser(ctx context.Context, req *user.GetUserRequest) (*user.User, error) {
    u, err := s.db.GetUser(ctx, req.Id)
    if err != nil {
        return nil, err
    }
    return &user.User{
        Id:    u.ID,
        Name:  u.Name,
        Email: u.Email,
    }, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    grpcServer := grpc.NewServer()
    user.RegisterUserServiceServer(grpcServer, &userService{})
    grpcServer.Serve(lis)
}

Error Handling

import (
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func (s *userService) GetUser(ctx context.Context, req *user.GetUserRequest) (*user.User, error) {
    user, err := s.db.GetUser(req.Id)
    if err == ErrNotFound {
        return nil, status.Error(codes.NotFound, "user not found")
    }
    {
        return nil if err != nil, status.Error(codes.Internal, "internal error")
    }
    return user, nil
}

// Client handling
resp, err := client.GetUser(ctx, req)
if err != nil {
    st, _ := status.FromError(err)
    switch st.Code() {
    case codes.NotFound:
        return fmt.Errorf("not found")
    case codes.Internal:
        return fmt.Errorf("server error")
    }
}

Streaming Implementation

Server Streaming

func (s *notificationService) StreamNotifications(req *user.NotificationRequest, stream user.NotificationService_StreamNotificationsServer) error {
    channel := s.notifier.Subscribe(req.UserId)
    defer s.notifier.Unsubscribe(req.UserId, channel)
    
    for {
        select {
        case notification := <-channel:
            if err := stream.Send(notification); err != nil {
                return err
            }
        case <-stream.Context().Done():
            return stream.Context().Err()
        }
    }
}

Bi-directional Streaming

func (s *chatService) Chat(stream user.ChatService_ChatServer) error {
    for {
        msg, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }
        
        response := &user.ChatMessage{
            Content: "Echo: " + msg.Content,
        }
        if err := stream.Send(response); err != nil {
            return err
        }
    }
}

REST to gRPC Migration

gRPC-JSON Gateway

import "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"

func main() {
    mux := runtime.NewServeMux()
    opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
    user.RegisterUserServiceHandlerFromEndpoint(ctx, mux, "localhost:50051", opts)
    http.ListenAndServe(":8080", mux)
}

Proto with HTTP Annotations

import "google/api/annotations.proto";

service UserService {
  rpc GetUser (GetUserRequest) returns (User) {
    option (google.api.http) = {
      get: "/api/v1/users/{id}"
    };
  }
  
  rpc CreateUser (CreateUserRequest) returns (User) {
    option (google.api.http) = {
      post: "/api/v1/users"
      body: "*"
    };
  }
}

Best Practices

Proto Design

  • Use versioned messages (UserV1, UserV2)
  • Use enums for status fields
  • Add comments to all fields
  • Use timestamps for date fields

Performance

  • Enable connection pooling
  • Use keepalive parameters
  • Enable compression for large payloads

External Resources


Comments