102. Case Study: Building a gRPC Server in Go and a Client in Node.js
102. Case Study: Building a gRPC Server in Go and a Client in Node.js
These days, microservices architecture is becoming increasingly popular for building large-scale applications. One effective way for services to communicate with one another is by using the gRPC protocol. gRPC enables fast, efficient data exchange and delivers a solid development experience by using Protobuf (Protocol Buffers) as its contract definition.
In this article, I’ll share a case study on how to build a gRPC server using Go and consume it with a simple client in Node.js. This project is perfect for anyone who wants to see cross-language interoperability with gRPC in practice, as well as understand the full-stack workflow comprehensively.
What are we going to build?
Imagine we’re going to create a simple microservice called UserService. This service has just one endpoint: GetUser, which serves as the RPC method for retrieving a user’s profile based on their user ID.
Here’s the simple network architecture:
flowchart TD
Client(Node.js) -->|gRPC call| GoServer(UserService)
GoServer(UserService) -->|Response| Client(Node.js)
1. Setting Up the Protocol Contract (user.proto)
First, we define the API contract with a Protobuf file named user.proto:
1syntax = "proto3";
2
3package user;
4
5service UserService {
6 rpc GetUser (GetUserRequest) returns (GetUserResponse);
7}
8
9message GetUserRequest {
10 string id = 1;
11}
12
13message GetUserResponse {
14 string id = 1;
15 string name = 2;
16 string email = 3;
17}Save this file in your project’s proto folder.
2. Building the gRPC Server in Go
2.1. Generating Stubs from .proto
First, install the dependencies in Go (the main module: google.golang.org/grpc, along with the protoc-gen-go and protoc-gen-go-grpc plugins):
1go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
2go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latestGenerate the Go code from the proto file:
1protoc --go_out=. --go-grpc_out=. proto/user.protoAfter generation, the project structure will look like this:
1.
2├── proto/
3│ └── user.proto
4├── user/
5│ ├── user.pb.go
6│ └── user_grpc.pb.go
7└── main.go2.2. Implementing UserService in Go
File: main.go
1package main
2
3import (
4 "context"
5 "log"
6 "net"
7
8 pb "your-module-name/user" // import the generated package
9
10 "google.golang.org/grpc"
11)
12
13type server struct {
14 pb.UnimplementedUserServiceServer
15}
16
17func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
18 // Simulation: Retrieve user by ID
19 users := map[string]struct{ name, email string }{
20 "1": {"Alice", "alice@mail.com"},
21 "2": {"Bob", "bob@mail.com"},
22 }
23 user, found := users[req.Id]
24 if !found {
25 return nil, grpc.Errorf(404, "User not found")
26 }
27
28 return &pb.GetUserResponse{
29 Id: req.Id,
30 Name: user.name,
31 Email: user.email,
32 }, nil
33}
34
35func main() {
36 lis, err := net.Listen("tcp", ":50051")
37 if err != nil {
38 log.Fatalf("failed to listen: %v", err)
39 }
40 grpcServer := grpc.NewServer()
41 pb.RegisterUserServiceServer(grpcServer, &server{})
42 log.Println("🚀 gRPC server running on :50051")
43 if err := grpcServer.Serve(lis); err != nil {
44 log.Fatalf("failed to serve: %v", err)
45 }
46}A brief explanation:
- The Go server above runs a single RPC:
GetUser. - The data is simulated using a map.
- Run it with
go run main.go
3. Building a Simple gRPC Client in Node.js
gRPC supports many languages, one of which is JavaScript. We’ll use the @grpc/grpc-js and @grpc/proto-loader packages.
3.1. Install Dependencies
1npm init -y
2npm install @grpc/grpc-js @grpc/proto-loader3.2. Implementing the Client
File: client.js
1const grpc = require('@grpc/grpc-js');
2const protoLoader = require('@grpc/proto-loader');
3
4// Load proto file
5const packageDefinition = protoLoader.loadSync('proto/user.proto', {
6 keepCase: true,
7 longs: String,
8 enums: String,
9 defaults: true,
10 oneofs: true,
11});
12const userProto = grpc.loadPackageDefinition(packageDefinition).user;
13
14// Create client
15const client = new userProto.UserService('localhost:50051',
16 grpc.credentials.createInsecure()
17);
18
19// Call GetUser
20const userId = "1";
21client.GetUser({ id: userId }, (error, response) => {
22 if (error) {
23 console.error('Error: ', error.message);
24 return;
25 }
26 console.log('User Profile:', response);
27});Run the Go server, then in a new terminal run the Node.js client with:
1node client.jsOutput:
1User Profile: { id: '1', name: 'Alice', email: 'alice@mail.com' }4. Simulation and Comparison Table
Let’s look at a communication simulation and a theoretical latency comparison (when compared against REST/JSON):
| Aspect | gRPC (Protobuf) | REST (JSON) |
|---|---|---|
| Parsing overhead | Low | Higher |
| Payload size | Very compact | Larger |
| Compatibility | Multi-language | Multi-language |
| Error handling | Status via code | HTTP code |
| Tooling | Requires proto | Swagger/JSON |
| Streaming | Built-in | Needs custom |
5. The gRPC Workflow in This Case Study
sequenceDiagram
participant NodeClient as Client (Node.js)
participant GoServer as Server (Go)
NodeClient->>GoServer: GetUser(Request{ id: "1" })
GoServer-->>NodeClient: GetUserResponse{ id: "1", name: "Alice", email: "alice@mail.com" }
6. Tips for Cross-Language Development
Keep Your
.protoin Sync
Make sure the exact same Protobuf file is used across all languages. Use a submodule or tools like buf.build .Error Handling
Inject error codes via gRPC so the client can troubleshoot more easily.Codegen Automation
Use a Makefile, an npm script, or a Taskfile so that code stubs are regenerated automatically whenever the schema changes.
Conclusion
In this case study, we’ve proven that gRPC is incredibly powerful for building distributed, cross-language microservices. A server in Go, a client in Node.js — all of them speaking Protobuf as their common language.
Beyond being more economical with bandwidth and RAM, the gRPC pipeline is also type-safe and makes scaling much easier down the road. Next, you can build out additional features such as authentication, interceptors, or use streaming for real-time data pipelines.
Happy coding, and I hope this short hands-on article is something you can apply right away in your next microservice project! 🚀
References:
Interested in exploring further, or have questions? Let’s discuss in the comments section!