Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
18 Sep 2025 · 5 min read ·Article 102 / 110
Go

102. Case Study: Building a gRPC Server in Go and a Client in Node.js

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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:

MERMAID
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:

protobuf
 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.

Danger
Best Practice: Always use Protobuf for language interoperability and backward compatibility when your data schema changes.

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):

bash
1go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
2go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Generate the Go code from the proto file:

bash
1protoc --go_out=. --go-grpc_out=. proto/user.proto

After generation, the project structure will look like this:

text
1.
2├── proto/
3│   └── user.proto
4├── user/
5│   ├── user.pb.go
6│   └── user_grpc.pb.go
7└── main.go

2.2. Implementing UserService in Go

File: main.go

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

bash
1npm init -y
2npm install @grpc/grpc-js @grpc/proto-loader

3.2. Implementing the Client

File: client.js

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:

bash
1node client.js

Output:

text
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):

AspectgRPC (Protobuf)REST (JSON)
Parsing overheadLowHigher
Payload sizeVery compactLarger
CompatibilityMulti-languageMulti-language
Error handlingStatus via codeHTTP code
ToolingRequires protoSwagger/JSON
StreamingBuilt-inNeeds custom

5. The gRPC Workflow in This Case Study

MERMAID
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 .proto in 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!

Related Articles

💬 Comments