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;
// Simple message
message User {
string id = 1;
string name = 2;
string email = 3;
int32 age = 4;
bool active = 5;
}
// Nested messages
message Order {
string id = 1;
User customer = 2;
repeated Item items = 3;
Status status = 4;
google.protobuf.Timestamp created_at = 5;
}
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 |
Repeated, Optional, and Maps
// Arrays
message User {
repeated string roles = 1; // []string
repeated Order orders = 2; // []Order
}
// Optional fields (Proto3)
message User {
// New optional syntax (Proto3.15+)
optional string nickname = 1;
// Or use wrapper types
google.protobuf.StringValue nickname = 1;
}
// Maps
message UserPreferences {
map<string, string> settings = 1;
map<string, int32> limits = 2;
}
gRPC Service Definition
Simple RPC
// Unary RPC - like REST endpoints
service UserService {
// Get a user by ID
rpc GetUser (GetUserRequest) returns (User);
// Create a new user
rpc CreateUser (CreateUserRequest) returns (User);
// Update a user
rpc UpdateUser (UpdateUserRequest) returns (User);
// Delete a user
rpc DeleteUser (DeleteUserRequest) returns (Empty);
// List users
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
}
message GetUserRequest {
string id = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
int32 age = 3;
}
message UpdateUserRequest {
string id = 1;
User user = 2;
}
message DeleteUserRequest {
string id = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2;
}
// Empty response
message Empty {}
Server Streaming
// Server streams responses
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;
google.protobuf.Timestamp timestamp = 4;
}
Client Streaming
// Client streams requests
service UploadService {
rpc Upload stream UploadRequest returns (UploadResponse);
}
message UploadRequest {
string filename = 1;
bytes chunk = 2;
int32 chunk_index = 3;
}
message UploadResponse {
string file_id = 1;
int64 size = 2;
bool success = 3;
}
Bi-directional Streaming
// Both client and server stream
service ChatService {
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
message ChatMessage {
string id = 1;
string user_id = 2;
string content = 3;
google.protobuf.Timestamp timestamp = 4;
}
Code Generation
Setup
# Install protoc and Go plugin
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
# Generate code
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/user.proto
Generated Go Code
// Generated interface
type UserServiceClient interface {
GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error)
CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*User, error)
UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.Option) (*User, error)
DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*Empty, error)
ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error)
}
// Server implementation
type UserServiceServer interface {
GetUser(context.Context, *GetUserRequest) (*User, error)
CreateUser(context.Context, *CreateUserRequest) (*User, error)
UpdateUser(context.Context, *UpdateUserRequest) (*User, error)
DeleteUser(context.Context, *DeleteUserRequest) (*Empty, error)
ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error)
mustEmbedUnimplementedUserServiceServer()
}
Server Implementation
package main
import (
"context"
"net"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/timestamppb"
"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,
Age: int32(u.Age),
Active: u.Active,
CreatedAt: timestamppb.New(u.CreatedAt),
}, nil
}
func (s *userService) CreateUser(ctx context.Context, req *user.CreateUserRequest) (*user.User, error) {
u := &User{
Name: req.Name,
Email: req.Email,
Age: int(req.Age),
}
created, err := s.db.CreateUser(ctx, u)
if err != nil {
return nil, err
}
return &user.User{
Id: created.ID,
Name: created.Name,
Email: created.Email,
Age: int32(created.Age),
Active: true,
CreatedAt: timestamppb.New(created.CreatedAt),
}, 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{db: db})
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Client Implementation
package main
import (
"context"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"myapp/proto/user"
)
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := user.NewUserServiceClient(conn)
// Call GetUser
ctx := context.Background()
user, err := client.GetUser(ctx, &user.GetUserRequest{Id: "123"})
if err != nil {
log.Fatalf("could not get user: %v", err)
}
log.Printf("User: %s <%s>", user.Name, user.Email)
}
Error Handling
// Define error codes
enum ErrorCode {
ERROR_CODE_UNSPECIFIED = 0;
ERROR_CODE_NOT_FOUND = 1;
ERROR_CODE_INVALID_INPUT = 2;
ERROR_CODE_UNAUTHORIZED = 3;
ERROR_CODE_INTERNAL = 4;
}
message Error {
ErrorCode code = 1;
string message = 2;
repeated string details = 3;
}
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// Server-side error
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")
}
if err == ErrInvalidInput {
return nil, status.Error(codes.InvalidArgument, "invalid user ID")
}
if err != nil {
return nil, status.Error(codes.Internal, "internal error")
}
return user, nil
}
// Client-side handling
resp, err := client.GetUser(ctx, &user.GetUserRequest{Id: "123"})
if err != nil {
st, ok := status.FromError(err)
if !ok {
return err // Not a gRPC error
}
switch st.Code() {
case codes.NotFound:
return fmt.Errorf("user not found: %s", st.Message())
case codes.InvalidArgument:
return fmt.Errorf("invalid input: %s", st.Message())
case codes.Internal:
return fmt.Errorf("server error: %s", st.Message())
default:
return fmt.Errorf("unknown error: %s", st.Message())
}
}
Metadata and Authentication
Metadata
// Server - read metadata
func (s *userService) GetUser(ctx context.Context, req *user.GetUserRequest) (*user.User, error) {
md, ok := metadata.FromIncomingContext(ctx)
if ok {
if token := md.Get("authorization"); len(token) > 0 {
log.Printf("Token: %s", token[0])
}
}
userID := ctx.Value("userID").(string)
// Use userID for authorization
}
// Client - send metadata
ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs(
"authorization", "Bearer "+token,
))
resp, err := client.GetUser(ctx, req)
Authentication Interceptor
// Server interceptor
func authInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// Extract token from metadata
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
token := md.Get("authorization")
if len(token) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing token")
}
// Validate token
userID, err := validateToken(token[0])
if err != nil {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
// Add user ID to context
ctx = context.WithValue(ctx, "userID", userID)
return handler(ctx, req)
}
func main() {
server := grpc.NewServer(
grpc.UnaryInterceptor(authInterceptor),
)
}
Streaming Implementation
Server Streaming
// Server
func (s *notificationService) StreamNotifications(req *user.NotificationRequest, stream user.NotificationService_StreamNotificationsServer) error {
userID := req.UserId
// Subscribe to notifications
channel := s.notifier.Subscribe(userID)
defer s.notifier.Unsubscribe(userID, channel)
for {
select {
case notification := <-channel:
if err := stream.Send(notification); err != nil {
return err
}
case <-stream.Context().Done():
return stream.Context().Err()
}
}
}
// Client
stream, err := client.StreamNotifications(context.Background(), &user.NotificationRequest{UserId: "123"})
if err != nil {
log.Fatal(err)
}
for {
notification, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
fmt.Printf("Notification: %s\n", notification.Message)
}
Bi-directional Streaming
// Server
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
}
// Process message
response := &user.ChatMessage{
Id: uuid.New().String(),
UserId: msg.UserId,
Content: "Echo: " + msg.Content,
Timestamp: timestamppb.Now(),
}
if err := stream.Send(response); err != nil {
return err
}
}
}
// Client
stream, err := client.Chat(context.Background())
if err != nil {
log.Fatal(err)
}
// Send messages
go func() {
for {
stream.Send(&user.ChatMessage{
UserId: "user123",
Content: "Hello!",
})
time.Sleep(time.Second)
}
}()
// Receive messages
go func() {
for {
msg, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
fmt.Printf("Received: %s\n", msg.Content)
}
}()
stream.CloseSend()
// Wait for completion
<-stream.Context().Done()
REST to gRPC Migration
gRPC-JSON Gateway
import (
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
)
func main() {
// gRPC server
grpcServer := grpc.NewServer()
user.RegisterUserServiceServer(grpcServer, &userService{})
// HTTP gateway
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
user.RegisterUserServiceHandlerFromEndpoint(context.Background(), mux, "localhost:50051", opts)
// Combined server
httpMux := http.NewServeMux()
httpMux.Handle("/", mux)
http.ListenAndServe(":8080", httpMux)
}
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: "*"
};
}
rpc UpdateUser (UpdateUserRequest) returns (User) {
option (google.api.http) = {
put: "/api/v1/users/{id}"
body: "user"
};
}
rpc DeleteUser (DeleteUserRequest) returns (Empty) {
option (google.api.http) = {
delete: "/api/v1/users/{id}"
};
}
}
Best Practices
Proto Design
// Good: Clear, versioned
message UserV1 {
string id = 1;
string name = 2;
string email = 3;
}
// Good: Use enums for status
enum Status {
STATUS_UNSPECIFIED = 0;
STATUS_ACTIVE = 1;
STATUS_INACTIVE = 2;
}
// Good: Use timestamps
import "google/protobuf/timestamp.proto";
message Event {
google.protobuf.Timestamp created_at = 1;
}
// Good: Add comments
// This message represents a user in the system
message User {
// Unique identifier
string id = 1;
}
Performance Tips
// Connection pooling
conn, err := grpc.Dial(
"server:50051",
grpc.WithBalancer(roundrobin.NewResolver()),
grpc.WithTransportCredentials(credentials),
)
// Reuse connections
grpc.DialContext(ctx, "server:50051",
grpc.WithTransportCredentials(creds),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 20 * time.Second,
Timeout: 10 * time.Second,
}),
)
// Enable compression
grpc.Dial("server:50051",
grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)),
)
External Resources
Related Articles
- REST API Design Best Practices
- API Authentication Methods
- API Gateway Patterns
- GraphQL vs REST vs tRPC
Comments