Skip to main content

gRPC APIs

gRPC is a high-performance, open-source RPC framework that uses Protocol Buffers for serialization and HTTP/2 for transport.

gRPC Fundamentals

Core Components

Protocol Buffers

  • Schema definition: .proto files define services and messages
  • Code generation: Auto-generate client and server code
  • Binary serialization: Efficient data encoding
  • Language agnostic: Support for multiple programming languages

HTTP/2 Transport

  • Multiplexing: Multiple streams over single connection
  • Binary framing: Efficient data transfer
  • Header compression: Reduced overhead
  • Server push: Proactive response sending

Service Definition

  • RPC methods: Define remote procedure calls
  • Message types: Strongly typed data structures
  • Streaming support: Client, server, and bidirectional streaming
  • Package organization: Namespace management

gRPC vs REST

FeaturegRPCREST
ProtocolHTTP/2HTTP/1.1, HTTP/2
SerializationProtocol BuffersJSON, XML
Code GenerationAutomaticManual
StreamingBuilt-inLimited
PerformanceHighMedium
Browser SupportLimitedNative
ToolingGeneratedManual

Protocol Buffers

Message Definition

syntax = "proto3";

package user;

message User {
string id = 1;
string name = 2;
string email = 3;
UserStatus status = 4;
repeated Order orders = 5;
google.protobuf.Timestamp created_at = 6;
google.protobuf.Timestamp updated_at = 7;
}

message Order {
string id = 1;
string user_id = 2;
double total = 3;
string status = 4;
repeated OrderItem items = 5;
google.protobuf.Timestamp created_at = 6;
}

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

enum UserStatus {
USER_STATUS_UNSPECIFIED = 0;
USER_STATUS_ACTIVE = 1;
USER_STATUS_INACTIVE = 2;
USER_STATUS_SUSPENDED = 3;
}

Service Definition

service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);

// Streaming methods
rpc StreamUsers(StreamUsersRequest) returns (stream UserResponse);
rpc CreateUserStream(stream CreateUserRequest) returns (CreateUserResponse);
rpc BidirectionalStream(stream BidirectionalRequest) returns (stream BidirectionalResponse);
}

message GetUserRequest {
string user_id = 1;
}

message GetUserResponse {
User user = 1;
}

message ListUsersRequest {
int32 limit = 1;
int32 offset = 2;
UserStatus status = 3;
}

message ListUsersResponse {
repeated User users = 1;
int32 total_count = 2;
}

gRPC Patterns

Unary RPC

Request-Response: Single request, single response

rpc GetUser(GetUserRequest) returns (GetUserResponse);

Server Streaming

Single Request, Multiple Responses: Client sends one request, server streams back responses

rpc StreamUsers(StreamUsersRequest) returns (stream UserResponse);

Client Streaming

Multiple Requests, Single Response: Client streams requests, server responds once

rpc CreateUserStream(stream CreateUserRequest) returns (CreateUserResponse);

Bidirectional Streaming

Multiple Requests, Multiple Responses: Both client and server stream independently

rpc BidirectionalStream(stream BidirectionalRequest) returns (stream BidirectionalResponse);

Implementation Examples

gRPC Server (Node.js)

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const PROTO_PATH = './user.proto';
const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const userProto = grpc.loadPackageDefinition(packageDefinition).user;

class UserServiceImpl {
async getUser(call, callback) {
try {
const { user_id } = call.request;
const user = await User.findById(user_id);

if (!user) {
callback({
code: grpc.status.NOT_FOUND,
details: 'User not found'
});
return;
}

callback(null, { user });
} catch (error) {
callback({
code: grpc.status.INTERNAL,
details: error.message
});
}
}

async listUsers(call, callback) {
try {
const { limit = 10, offset = 0, status } = call.request;
const users = await User.find(status ? { status } : {})
.limit(limit)
.skip(offset);

const totalCount = await User.countDocuments(status ? { status } : {});

callback(null, { users, total_count: totalCount });
} catch (error) {
callback({
code: grpc.status.INTERNAL,
details: error.message
});
}
}

async createUser(call, callback) {
try {
const { name, email, status = 'USER_STATUS_ACTIVE' } = call.request;
const user = await User.create({
name,
email,
status,
created_at: new Date(),
updated_at: new Date()
});

callback(null, { user });
} catch (error) {
callback({
code: grpc.status.INVALID_ARGUMENT,
details: error.message
});
}
}

async streamUsers(call) {
try {
const { limit = 10, status } = call.request;
const users = await User.find(status ? { status } : {}).limit(limit);

for (const user of users) {
call.write({ user });
}

call.end();
} catch (error) {
call.destroy({
code: grpc.status.INTERNAL,
details: error.message
});
}
}
}

const server = new grpc.Server();
server.addService(userProto.UserService.service, new UserServiceImpl());

server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure())
.then(() => {
server.start();
console.log('gRPC server running on port 50051');
})
.catch(error => {
console.error('Failed to start server:', error);
});

gRPC Client (Node.js)

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const PROTO_PATH = './user.proto';
const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const userProto = grpc.loadPackageDefinition(packageDefinition).user;

const client = new userProto.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);

// Unary call
function getUser(userId) {
return new Promise((resolve, reject) => {
client.getUser({ user_id: userId }, (error, response) => {
if (error) {
reject(error);
return;
}
resolve(response.user);
});
});
}

// Server streaming
function streamUsers(limit = 10) {
const call = client.streamUsers({ limit });

call.on('data', (response) => {
console.log('Received user:', response.user);
});

call.on('end', () => {
console.log('Stream ended');
});

call.on('error', (error) => {
console.error('Stream error:', error);
});
}

// Usage examples
async function main() {
try {
// Get single user
const user = await getUser('123');
console.log('User:', user);

// Stream users
streamUsers(5);
} catch (error) {
console.error('Error:', error);
}
}

main();

gRPC Server (Go)

package main

import (
"context"
"log"
"net"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"

pb "path/to/proto/user"
)

type server struct {
pb.UnimplementedUserServiceServer
}

func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
userID := req.GetUserId()

// Simulate database lookup
user := &pb.User{
Id: userID,
Name: "John Doe",
Email: "john@example.com",
Status: pb.UserStatus_USER_STATUS_ACTIVE,
CreatedAt: timestamppb.Now(),
UpdatedAt: timestamppb.Now(),
}

return &pb.GetUserResponse{User: user}, nil
}

func (s *server) ListUsers(ctx context.Context, req *pb.ListUsersRequest) (*pb.ListUsersResponse, error) {
limit := req.GetLimit()
if limit == 0 {
limit = 10
}

// Simulate database lookup
users := make([]*pb.User, 0, limit)
for i := 0; i < int(limit); i++ {
users = append(users, &pb.User{
Id: fmt.Sprintf("user_%d", i),
Name: fmt.Sprintf("User %d", i),
Email: fmt.Sprintf("user%d@example.com", i),
Status: pb.UserStatus_USER_STATUS_ACTIVE,
CreatedAt: timestamppb.Now(),
UpdatedAt: timestamppb.Now(),
})
}

return &pb.ListUsersResponse{
Users: users,
TotalCount: int32(len(users)),
}, nil
}

func (s *server) StreamUsers(req *pb.StreamUsersRequest, stream pb.UserService_StreamUsersServer) error {
limit := req.GetLimit()
if limit == 0 {
limit = 10
}

for i := 0; i < int(limit); i++ {
user := &pb.User{
Id: fmt.Sprintf("user_%d", i),
Name: fmt.Sprintf("User %d", i),
Email: fmt.Sprintf("user%d@example.com", i),
Status: pb.UserStatus_USER_STATUS_ACTIVE,
CreatedAt: timestamppb.Now(),
UpdatedAt: timestamppb.Now(),
}

if err := stream.Send(&pb.UserResponse{User: user}); err != nil {
return err
}

time.Sleep(100 * time.Millisecond) // Simulate work
}

return nil
}

func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}

s := grpc.NewServer()
pb.RegisterUserServiceServer(s, &server{})

log.Println("gRPC server listening on :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}

Advanced Features

Interceptors

// Logging interceptor
const loggingInterceptor = (options, nextCall) => {
return new InterceptingCall(nextCall(options), {
start: function(metadata, listener, next) {
console.log('Starting call:', metadata);
next(metadata, {
onReceiveMessage: function(message, next) {
console.log('Received message:', message);
next(message);
},
onReceiveStatus: function(status, next) {
console.log('Call completed with status:', status);
next(status);
}
});
}
});
};

// Authentication interceptor
const authInterceptor = (options, nextCall) => {
const metadata = options.metadata || new Map();
const token = metadata.get('authorization');

if (!token || !isValidToken(token)) {
throw new Error('Unauthorized');
}

return nextCall(options);
};

Load Balancing

// Round-robin load balancing
const addresses = [
'localhost:50051',
'localhost:50052',
'localhost:50053'
];

const client = new userProto.UserService(
addresses,
grpc.credentials.createInsecure(),
{
'grpc.load_balancing_policy': 'round_robin'
}
);

Deadlines and Timeouts

// Set deadline for call
const deadline = new Date(Date.now() + 5000); // 5 seconds
client.getUser({ user_id: '123' }, { deadline }, (error, response) => {
if (error) {
if (error.code === grpc.status.DEADLINE_EXCEEDED) {
console.log('Call timed out');
}
return;
}
console.log('User:', response.user);
});

Best Practices

Service Design

  • Idempotent methods: Design for retry safety
  • Error handling: Use appropriate gRPC status codes
  • Streaming: Use streaming for large datasets
  • Versioning: Plan for API evolution

Performance Optimization

  • Connection pooling: Reuse connections
  • Compression: Enable response compression
  • Deadlines: Set appropriate timeouts
  • Batching: Group related operations

Security

  • TLS: Use encrypted connections
  • Authentication: Implement proper auth
  • Authorization: Control access to methods
  • Metadata: Use metadata for auth tokens

Key Takeaway: gRPC provides high-performance, type-safe RPC communication with built-in streaming support, making it ideal for microservices and internal APIs where performance and type safety are critical.