Developing gRPC API for Web Application
gRPC is an RPC framework from Google based on Protocol Buffers and HTTP/2. Provides strict typing via .proto files, bidirectional streaming, and significantly lower overhead compared to JSON. Most appropriate for inter-service communication (microservices) and mobile clients with limited bandwidth.
Protocol Buffers
Service contract is defined in .proto files:
syntax = "proto3";
package articles.v1;
import "google/protobuf/timestamp.proto";
message Article {
string id = 1;
string title = 2;
string body = 3;
string author_id = 4;
repeated string tag_ids = 5;
google.protobuf.Timestamp created_at = 6;
}
message GetArticleRequest { string id = 1; }
message ListArticlesRequest {
int32 page = 1;
int32 limit = 2;
string status = 3;
}
message ListArticlesResponse {
repeated Article articles = 1;
int32 total = 2;
}
service ArticleService {
rpc GetArticle(GetArticleRequest) returns (Article);
rpc ListArticles(ListArticlesRequest) returns (ListArticlesResponse);
rpc CreateArticle(CreateArticleRequest) returns (Article);
rpc WatchArticle(GetArticleRequest) returns (stream Article); // server streaming
}
Code is generated for any language from .proto: protoc --go_out=. --go-grpc_out=..
Server Implementation (Go)
type ArticleServer struct {
pb.UnimplementedArticleServiceServer
db *sql.DB
}
func (s *ArticleServer) GetArticle(ctx context.Context, req *pb.GetArticleRequest) (*pb.Article, error) {
row := s.db.QueryRowContext(ctx, "SELECT id, title, body FROM articles WHERE id = $1", req.Id)
var a pb.Article
if err := row.Scan(&a.Id, &a.Title, &a.Body); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, status.Error(codes.NotFound, "article not found")
}
return nil, status.Error(codes.Internal, err.Error())
}
return &a, nil
}
// Server startup
lis, _ := net.Listen("tcp", ":50051")
grpcServer := grpc.NewServer(grpc.UnaryInterceptor(authInterceptor))
pb.RegisterArticleServiceServer(grpcServer, &ArticleServer{db: db})
grpcServer.Serve(lis)
Streaming
gRPC supports 4 interaction types:
// Unary (standard request/response)
rpc GetArticle(Request) returns (Response);
// Server streaming (one request → stream of responses)
rpc WatchUpdates(Request) returns (stream Event);
// Client streaming (stream of requests → one response)
rpc UploadChunks(stream Chunk) returns (UploadResult);
// Bidirectional streaming
rpc Chat(stream Message) returns (stream Message);
Server streaming is useful for: real-time notifications, exporting large data volumes, live results.
Interceptors
func authInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
token := md.Get("authorization")
if !validateToken(token[0]) {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
return handler(ctx, req)
}
Interceptors are middleware analogs: logging, auth, tracing (OpenTelemetry), rate limiting.
gRPC in Browser
gRPC doesn't work directly in browsers (HTTP/2 binary framing unavailable). Solutions:
- gRPC-Web—special protocol with Envoy proxy on server side
- Connect (Buf)—modern alternative, works with HTTP/1.1 and HTTP/2, compatible with gRPC
# Buf CLI for code generation
buf generate --template buf.gen.yaml
Documentation and Testing
-
gRPCurl—curl for gRPC:
grpcurl -plaintext localhost:50051 articles.v1.ArticleService/GetArticle - Postman—supports gRPC from version 9.7
- Buf Schema Registry—centralized storage of .proto files
When to Choose gRPC
gRPC is justified for:
- Inter-service communication within infrastructure
- Strict contract between teams
- Need for efficient binary protocol (IoT, mobile)
- Bidirectional streaming
REST/GraphQL is better for: public APIs, browser clients without grpc-web, quick prototypes.
Timelines
gRPC service (5–10 methods, auth interceptor, proto contract): 1–2 weeks. With bidirectional streaming, service mesh (Istio/Linkerd), gRPC-Web for browser: 2–4 weeks.







