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
Comments