When building microservices, choosing the right communication protocol is crucial. While REST and event-driven architectures have their place, gRPC offers a compelling alternative with strong typing, efficient binary serialization, and built-in support for streaming. In this guide, we’ll build a complete note-sharing application using gRPC microservices in Go, with a Vue.js frontend connected through a WebSocket gateway.

Why gRPC for Microservices?

gRPC brings several advantages to microservice architectures:

  • Strong Typing: Protocol Buffers provide type-safe contracts between services
  • Performance: Binary serialization is faster and more compact than JSON
  • Code Generation: Auto-generate client and server code from .proto files
  • Streaming: Built-in support for bidirectional streaming
  • Language Agnostic: Works across many programming languages
  • HTTP/2: Connection multiplexing, header compression, and server push

However, browsers can’t make native gRPC calls. That’s where our WebSocket gateway comes in.

System Architecture

We’ll build four components:

  1. User Service: gRPC service for user management (CRUD operations)
  2. Note Service: gRPC service for notes that calls User Service for validation
  3. API Gateway: gRPC-Web proxy with WebSocket support for browsers
  4. Frontend: Vue.js app using gRPC-Web client
%%{init: {'theme':'dark', 'themeVariables': {'primaryTextColor':'#e5e7eb','secondaryTextColor':'#e5e7eb','tertiaryTextColor':'#e5e7eb','textColor':'#e5e7eb','nodeTextColor':'#e5e7eb','edgeLabelText':'#e5e7eb','clusterTextColor':'#e5e7eb','actorTextColor':'#e5e7eb'}}}%% graph TB subgraph "Frontend" VueApp[Vue.js App
:3000] end subgraph "API Layer" Gateway[gRPC-Web Gateway
:8080
WebSocket Support] end subgraph "Microservices" UserSvc[User Service
:50051
gRPC] NoteSvc[Note Service
:50052
gRPC] end VueApp <-->|gRPC-Web
WebSocket| Gateway Gateway <-->|gRPC| UserSvc Gateway <-->|gRPC| NoteSvc NoteSvc -->|gRPC Call| UserSvc style Gateway fill:#4CAF50 style UserSvc fill:#2196F3 style NoteSvc fill:#FF9800 style VueApp fill:#42b883

Communication Flow

%%{init: {'theme':'dark', 'themeVariables': {'primaryTextColor':'#e5e7eb','secondaryTextColor':'#e5e7eb','tertiaryTextColor':'#e5e7eb','textColor':'#e5e7eb','nodeTextColor':'#e5e7eb','edgeLabelText':'#e5e7eb','clusterTextColor':'#e5e7eb','actorTextColor':'#e5e7eb'}}}%% sequenceDiagram participant Client as Vue.js Client participant Gateway as API Gateway participant NoteSvc as Note Service participant UserSvc as User Service rect rgb(40, 60, 80) Note Creating a Note Client->>Gateway: CreateNote (gRPC-Web) Gateway->>NoteSvc: CreateNote (gRPC) NoteSvc->>UserSvc: GetUser (gRPC) UserSvc-->>NoteSvc: User Details NoteSvc->>NoteSvc: Create & Enrich Note NoteSvc-->>Gateway: Note with User Info Gateway-->>Client: Note Response end rect rgb(60, 40, 80) Note Sharing a Note Client->>Gateway: ShareNote (gRPC-Web) Gateway->>NoteSvc: ShareNote (gRPC) NoteSvc->>UserSvc: GetUser (validate recipient) UserSvc-->>NoteSvc: User exists NoteSvc->>NoteSvc: Add user to shared list NoteSvc->>UserSvc: GetUser (for each shared user) UserSvc-->>NoteSvc: User details NoteSvc-->>Gateway: Enriched note Gateway-->>Client: Success response end rect rgb(40, 80, 60) Note Listing User Notes Client->>Gateway: ListNotesByUser (gRPC-Web) Gateway->>NoteSvc: ListNotesByUser (gRPC) NoteSvc->>UserSvc: GetUser (validate user) UserSvc-->>NoteSvc: User exists NoteSvc->>NoteSvc: Filter notes loop For each note NoteSvc->>UserSvc: GetUser (owner) NoteSvc->>UserSvc: GetUser (shared users) end NoteSvc-->>Gateway: Enriched notes list Gateway-->>Client: Notes array end

Step 1: Defining Protocol Buffers

Protocol Buffers are the foundation of gRPC. They define your API contract and data structures.

User Service Proto

syntax = "proto3";

package user;

option go_package = "github.com/colossus21/alamrafiul/projects/grpc-notes-app/proto/user";

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

message User {
  string id = 1;
  string username = 2;
  string email = 3;
  int64 created_at = 4;
  int64 updated_at = 5;
}

message CreateUserRequest {
  string username = 1;
  string email = 2;
}

message GetUserRequest {
  string id = 1;
}

message ListUsersRequest {
  int32 page = 1;
  int32 page_size = 2;
}

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

Note Service Proto

syntax = "proto3";

package note;

option go_package = "github.com/colossus21/alamrafiul/projects/grpc-notes-app/proto/note";

service NoteService {
  rpc CreateNote(CreateNoteRequest) returns (Note);
  rpc GetNote(GetNoteRequest) returns (Note);
  rpc ListNotes(ListNotesRequest) returns (ListNotesResponse);
  rpc ListNotesByUser(ListNotesByUserRequest) returns (ListNotesResponse);
  rpc UpdateNote(UpdateNoteRequest) returns (Note);
  rpc DeleteNote(DeleteNoteRequest) returns (DeleteNoteResponse);
  rpc ShareNote(ShareNoteRequest) returns (ShareNoteResponse);
}

message Note {
  string id = 1;
  string title = 2;
  string content = 3;
  string user_id = 4;
  UserInfo user = 5;                        // Enriched from User Service
  repeated string shared_with_user_ids = 6;
  repeated UserInfo shared_with_users = 7;  // Enriched from User Service
  int64 created_at = 8;
  int64 updated_at = 9;
}

message UserInfo {
  string id = 1;
  string username = 2;
  string email = 3;
}

Key Design Decision: The Note message includes both user IDs and enriched UserInfo. The Note Service fetches user details from the User Service to provide complete information in responses.

Step 2: Implementing the User Service

The User Service is a straightforward CRUD service using in-memory storage.

type UserServiceServer struct {
	pb.UnimplementedUserServiceServer
	users map[string]*pb.User
	mu    sync.RWMutex
}

func (s *UserServiceServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
	log.Printf("CreateUser called: username=%s, email=%s", req.Username, req.Email)

	if req.Username == "" || req.Email == "" {
		return nil, status.Error(codes.InvalidArgument, "username and email are required")
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	// Check for duplicates
	for _, user := range s.users {
		if user.Username == req.Username {
			return nil, status.Error(codes.AlreadyExists, "username already exists")
		}
		if user.Email == req.Email {
			return nil, status.Error(codes.AlreadyExists, "email already exists")
		}
	}

	now := time.Now().Unix()
	user := &pb.User{
		Id:        uuid.New().String(),
		Username:  req.Username,
		Email:     req.Email,
		CreatedAt: now,
		UpdatedAt: now,
	}

	s.users[user.Id] = user
	log.Printf("User created: id=%s, username=%s", user.Id, user.Username)
	return user, nil
}

func (s *UserServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	user, exists := s.users[req.Id]
	if !exists {
		return nil, status.Error(codes.NotFound, "user not found")
	}

	return user, nil
}

Starting the User Service

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "50051"
	}

	lis, err := net.Listen("tcp", fmt.Sprintf(":%s", port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	grpcServer := grpc.NewServer()
	userService := NewUserServiceServer()
	pb.RegisterUserServiceServer(grpcServer, userService)

	// Enable reflection for grpcurl testing
	reflection.Register(grpcServer)

	log.Printf("User Service starting on port %s", port)
	if err := grpcServer.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

Step 3: Implementing the Note Service

The Note Service demonstrates inter-service communication by calling the User Service.

Connecting to User Service

type NoteServiceServer struct {
	notepb.UnimplementedNoteServiceServer
	notes      map[string]*notepb.Note
	mu         sync.RWMutex
	userClient userpb.UserServiceClient  // gRPC client for User Service
}

func main() {
	userServiceAddr := os.Getenv("USER_SERVICE_ADDR")
	if userServiceAddr == "" {
		userServiceAddr = "localhost:50051"
	}

	// Connect to User Service via gRPC
	log.Printf("Connecting to User Service at %s", userServiceAddr)
	userConn, err := grpc.Dial(
		userServiceAddr,
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		log.Fatalf("failed to connect to user service: %v", err)
	}
	defer userConn.Close()

	userClient := userpb.NewUserServiceClient(userConn)
	noteService := NewNoteServiceServer(userClient)

	// ... rest of setup
}

Making RPC Calls to User Service

// getUserInfo fetches user information from the User Service
func (s *NoteServiceServer) getUserInfo(ctx context.Context, userID string) (*notepb.UserInfo, error) {
	user, err := s.userClient.GetUser(ctx, &userpb.GetUserRequest{Id: userID})
	if err != nil {
		return nil, err
	}

	return &notepb.UserInfo{
		Id:       user.Id,
		Username: user.Username,
		Email:    user.Email,
	}, nil
}

// enrichNoteWithUserInfo adds user information to a note
func (s *NoteServiceServer) enrichNoteWithUserInfo(ctx context.Context, note *notepb.Note) error {
	// Get owner info
	userInfo, err := s.getUserInfo(ctx, note.UserId)
	if err != nil {
		log.Printf("Warning: failed to get user info for user_id=%s: %v", note.UserId, err)
	} else {
		note.User = userInfo
	}

	// Get shared users info
	if len(note.SharedWithUserIds) > 0 {
		sharedUsers := make([]*notepb.UserInfo, 0, len(note.SharedWithUserIds))
		for _, userID := range note.SharedWithUserIds {
			userInfo, err := s.getUserInfo(ctx, userID)
			if err != nil {
				log.Printf("Warning: failed to get user info for shared user_id=%s: %v", userID, err)
				continue
			}
			sharedUsers = append(sharedUsers, userInfo)
		}
		note.SharedWithUsers = sharedUsers
	}

	return nil
}

Creating Notes with User Validation

func (s *NoteServiceServer) CreateNote(ctx context.Context, req *notepb.CreateNoteRequest) (*notepb.Note, error) {
	log.Printf("CreateNote called: title=%s, user_id=%s", req.Title, req.UserId)

	// Verify user exists by calling User Service
	_, err := s.userClient.GetUser(ctx, &userpb.GetUserRequest{Id: req.UserId})
	if err != nil {
		return nil, status.Error(codes.NotFound, "user not found")
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	now := time.Now().Unix()
	note := &notepb.Note{
		Id:                uuid.New().String(),
		Title:             req.Title,
		Content:           req.Content,
		UserId:            req.UserId,
		SharedWithUserIds: []string{},
		CreatedAt:         now,
		UpdatedAt:         now,
	}

	s.notes[note.Id] = note

	// Enrich with user info before returning
	if err := s.enrichNoteWithUserInfo(ctx, note); err != nil {
		log.Printf("Warning: failed to enrich note with user info: %v", err)
	}

	log.Printf("Note created: id=%s, title=%s", note.Id, note.Title)
	return note, nil
}

Sharing Notes

func (s *NoteServiceServer) ShareNote(ctx context.Context, req *notepb.ShareNoteRequest) (*notepb.ShareNoteResponse, error) {
	log.Printf("ShareNote called: note_id=%s, user_id=%s", req.NoteId, req.UserId)

	// Verify user exists before sharing
	_, err := s.userClient.GetUser(ctx, &userpb.GetUserRequest{Id: req.UserId})
	if err != nil {
		return nil, status.Error(codes.NotFound, "user not found")
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	note, exists := s.notes[req.NoteId]
	if !exists {
		return nil, status.Error(codes.NotFound, "note not found")
	}

	// Check if already shared
	for _, sharedUserID := range note.SharedWithUserIds {
		if sharedUserID == req.UserId {
			return nil, status.Error(codes.AlreadyExists, "note already shared with this user")
		}
	}

	note.SharedWithUserIds = append(note.SharedWithUserIds, req.UserId)
	note.UpdatedAt = time.Now().Unix()

	// Make a copy and enrich
	noteCopy := &notepb.Note{
		Id:                note.Id,
		Title:             note.Title,
		Content:           note.Content,
		UserId:            note.UserId,
		SharedWithUserIds: note.SharedWithUserIds,
		CreatedAt:         note.CreatedAt,
		UpdatedAt:         note.UpdatedAt,
	}

	if err := s.enrichNoteWithUserInfo(ctx, noteCopy); err != nil {
		log.Printf("Warning: failed to enrich note with user info: %v", err)
	}

	return &notepb.ShareNoteResponse{Success: true, Note: noteCopy}, nil
}

Step 4: Building the API Gateway

Browsers can’t make native gRPC calls because they don’t support HTTP/2 trailers. The gateway solves this by translating between gRPC-Web and gRPC.

Setting Up grpc-web

import (
	"github.com/improbable-eng/grpc-web/go/grpcweb"
	"github.com/rs/cors"
)

func main() {
	// Connect to backend services
	userConn, _ := grpc.Dial(userServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	noteConn, _ := grpc.Dial(noteServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))

	// Create a gRPC server
	grpcServer := grpc.NewServer()

	// Register proxy handlers
	userpb.RegisterUserServiceServer(grpcServer, &userServiceProxy{
		client: userpb.NewUserServiceClient(userConn),
	})
	notepb.RegisterNoteServiceServer(grpcServer, &noteServiceProxy{
		client: notepb.NewNoteServiceClient(noteConn),
	})

	// Wrap with grpc-web
	wrappedGrpc := grpcweb.WrapServer(grpcServer,
		grpcweb.WithCorsForRegisteredEndpointsOnly(false),
		grpcweb.WithWebsockets(true),
		grpcweb.WithWebsocketOriginFunc(func(req *http.Request) bool {
			return true // Allow all origins for development
		}),
	)

	// ... serve HTTP
}

Proxy Implementation

The proxy simply forwards requests to the actual services:

type userServiceProxy struct {
	userpb.UnimplementedUserServiceServer
	client userpb.UserServiceClient
}

func (p *userServiceProxy) CreateUser(ctx context.Context, req *userpb.CreateUserRequest) (*userpb.User, error) {
	return p.client.CreateUser(ctx, req)
}

func (p *userServiceProxy) GetUser(ctx context.Context, req *userpb.GetUserRequest) (*userpb.User, error) {
	return p.client.GetUser(ctx, req)
}

// ... other methods

HTTP Handler with CORS

handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
	if wrappedGrpc.IsGrpcWebRequest(req) ||
	   wrappedGrpc.IsAcceptableGrpcCorsRequest(req) ||
	   wrappedGrpc.IsGrpcWebSocketRequest(req) {
		wrappedGrpc.ServeHTTP(resp, req)
	} else {
		if req.URL.Path == "/health" {
			resp.WriteHeader(http.StatusOK)
			resp.Write([]byte("OK"))
			return
		}
		http.NotFound(resp, req)
	}
})

// Add CORS
corsHandler := cors.New(cors.Options{
	AllowedOrigins:   []string{"*"},
	AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
	AllowedHeaders:   []string{"*"},
	ExposedHeaders:   []string{"*"},
	AllowCredentials: true,
}).Handler(handler)

httpServer := &http.Server{
	Addr:    ":8080",
	Handler: corsHandler,
}

log.Printf("Gateway starting on port 8080")
httpServer.ListenAndServe()

Step 5: Vue.js Frontend with gRPC-Web

Simplified gRPC Client

For this demo, we use a simplified JSON-based approach. In production, you’d use the official gRPC-Web client:

class GrpcClient {
  constructor(baseUrl = 'http://localhost:8080') {
    this.baseUrl = baseUrl;
  }

  async callUnary(service, method, request) {
    const url = `${this.baseUrl}/${service}/${method}`;

    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/grpc-web+json',
        'X-Grpc-Web': '1',
      },
      body: JSON.stringify(request),
    });

    if (!response.ok) {
      const error = await response.text();
      throw new Error(error);
    }

    return await response.json();
  }
}

export const grpcClient = new GrpcClient();

User Service Client

export const userService = {
  async createUser(username, email) {
    return grpcClient.callUnary('user.UserService', 'CreateUser', {
      username,
      email,
    });
  },

  async getUser(id) {
    return grpcClient.callUnary('user.UserService', 'GetUser', {
      id,
    });
  },

  async listUsers(page = 1, pageSize = 10) {
    return grpcClient.callUnary('user.UserService', 'ListUsers', {
      page,
      pageSize,
    });
  },

  async deleteUser(id) {
    return grpcClient.callUnary('user.UserService', 'DeleteUser', {
      id,
    });
  },
};

Note Service Client

export const noteService = {
  async createNote(title, content, userId) {
    return grpcClient.callUnary('note.NoteService', 'CreateNote', {
      title,
      content,
      userId,
    });
  },

  async listNotesByUser(userId, page = 1, pageSize = 10) {
    return grpcClient.callUnary('note.NoteService', 'ListNotesByUser', {
      userId,
      page,
      pageSize,
    });
  },

  async shareNote(noteId, userId) {
    return grpcClient.callUnary('note.NoteService', 'ShareNote', {
      noteId,
      userId,
    });
  },
};

Vue Component Example

<script setup>
import { ref, onMounted } from 'vue';
import { userService } from '../services/grpc-client';

const users = ref([]);
const loading = ref(false);

const loadUsers = async () => {
  loading.value = true;
  try {
    const response = await userService.listUsers();
    users.value = response.users || [];
  } catch (err) {
    console.error('Error loading users:', err);
  } finally {
    loading.value = false;
  }
};

const createUser = async (username, email) => {
  try {
    await userService.createUser(username, email);
    await loadUsers();
  } catch (err) {
    alert('Error creating user: ' + err.message);
  }
};

onMounted(loadUsers);
</script>

Step 6: Docker Deployment

Docker Compose Configuration

version: '3.8'

services:
  user-service:
    build:
      context: .
      dockerfile: services/user-service/Dockerfile
    ports:
      - "50051:50051"
    environment:
      - PORT=50051
    networks:
      - grpc-network

  note-service:
    build:
      context: .
      dockerfile: services/note-service/Dockerfile
    ports:
      - "50052:50052"
    environment:
      - PORT=50052
      - USER_SERVICE_ADDR=user-service:50051
    depends_on:
      - user-service
    networks:
      - grpc-network

  gateway:
    build:
      context: .
      dockerfile: gateway/Dockerfile
    ports:
      - "8080:8080"
    environment:
      - PORT=8080
      - USER_SERVICE_ADDR=user-service:50051
      - NOTE_SERVICE_ADDR=note-service:50052
    depends_on:
      - user-service
      - note-service
    networks:
      - grpc-network

  frontend:
    build:
      context: ./frontend
    ports:
      - "3000:3000"
    depends_on:
      - gateway
    networks:
      - grpc-network

networks:
  grpc-network:
    driver: bridge

Multi-Stage Dockerfile for Go Services

FROM golang:1.21-alpine AS builder

WORKDIR /app

# Install protoc
RUN apk add --no-cache protobuf-dev git

# Copy go mod files
COPY services/user-service/go.mod services/user-service/go.sum ./
RUN go mod download

# Install protoc plugins
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# Generate protobuf code
COPY proto /proto
RUN mkdir -p pb && \
    protoc --go_out=pb --go_opt=paths=source_relative \
    --go-grpc_out=pb --go-grpc_opt=paths=source_relative \
    /proto/user.proto

# Build
COPY services/user-service/*.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o user-service

# Final stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/user-service .

EXPOSE 50051
CMD ["./user-service"]

Testing with grpcurl

grpcurl is like curl for gRPC services:

# List available services
grpcurl -plaintext localhost:50051 list

# Create a user
grpcurl -plaintext -d '{
  "username": "alice",
  "email": "[email protected]"
}' localhost:50051 user.UserService/CreateUser

# Response:
{
  "id": "3f2a8b4c-...",
  "username": "alice",
  "email": "[email protected]",
  "createdAt": "1706400000",
  "updatedAt": "1706400000"
}

# Create a note
grpcurl -plaintext -d '{
  "title": "My First Note",
  "content": "Hello gRPC!",
  "userId": "3f2a8b4c-..."
}' localhost:50052 note.NoteService/CreateNote

# List notes by user
grpcurl -plaintext -d '{
  "userId": "3f2a8b4c-...",
  "page": 1,
  "pageSize": 10
}' localhost:50052 note.NoteService/ListNotesByUser

gRPC vs Event-Driven: When to Use What?

Use gRPC When:

  1. Strong Typing Matters: You want compile-time type safety
  2. Low Latency Required: Binary serialization is faster
  3. Request/Reply Pattern: You need synchronous responses
  4. Internal Services: Microservices within your data center
  5. Streaming: You need bidirectional streaming support

Use Event-Driven (NATS) When:

  1. Loose Coupling: Services shouldn’t know about each other
  2. Async Operations: You don’t need immediate responses
  3. Broadcasting: Multiple services need the same data
  4. Resilience: Failures shouldn’t cascade
  5. Event Sourcing: You need an audit trail

Architecture Comparison

Aspect gRPC NATS Event-Driven
Communication Direct RPC Publish/Subscribe
Coupling Tighter Looser
Response Time Synchronous Asynchronous
Type Safety Strong (protobuf) Flexible (JSON)
Service Discovery Address-based Topic-based
Failure Handling Immediate error Retry/DLQ
Use Case Request/Reply Fire-and-forget
Complexity Lower initial Higher initial
Scalability Vertical Horizontal

Key Takeaways

  1. Protocol Buffers provide strong typing and efficient serialization
  2. Service-to-Service gRPC enables direct, type-safe communication
  3. gRPC-Web Gateway bridges the gap between browsers and gRPC
  4. WebSocket Support enables real-time updates in browsers
  5. Inter-Service Calls demonstrate how microservices collaborate
  6. Docker Compose simplifies multi-service orchestration

Performance Considerations

gRPC Advantages:

  • Binary Protocol: 30-50% smaller payloads than JSON
  • HTTP/2: Connection multiplexing reduces latency
  • Streaming: Efficient for large data transfers
  • Code Generation: No runtime reflection overhead

Watch Out For:

  • Browser Compatibility: Requires grpc-web proxy
  • Debugging: Binary format harder to inspect than JSON
  • Load Balancing: HTTP/2 requires special handling
  • Error Handling: gRPC status codes differ from HTTP

Next Steps

To enhance this application:

  1. Add Authentication: Implement JWT tokens in gateway
  2. Database Integration: Replace in-memory storage with PostgreSQL
  3. Streaming: Add server streaming for real-time updates
  4. Error Handling: Implement retry logic and circuit breakers
  5. Monitoring: Add Prometheus metrics and distributed tracing
  6. TLS: Enable encryption for production
  7. Load Balancing: Implement client-side load balancing

Source Code

The complete source code is available in the projects/grpc-notes-app directory, including:

  • Protocol Buffer definitions
  • Go microservices with full CRUD operations
  • gRPC-Web gateway with WebSocket support
  • Vue.js frontend with Tailwind CSS
  • Docker Compose orchestration
  • Makefiles for code generation

Conclusion

gRPC provides a powerful foundation for microservices architecture. While it requires more upfront setup than REST, the benefits of strong typing, performance, and built-in streaming make it ideal for service-to-service communication. The WebSocket gateway pattern bridges the gap to browser clients, giving you the best of both worlds.

Whether you choose gRPC, event-driven architecture, or a hybrid approach depends on your specific requirements. This guide demonstrates that with the right architecture, you can build efficient, type-safe microservices that scale.

Happy coding!