Microservices Interaction Using gRPC in Node.js: A Complete Walkthrough
In this blog, we’ll take a deep dive into how gRPC facilitates communication between two Node.js-based microservices. We’ll walk you through the complete interaction flow, covering both the conceptual understanding and practical implementation.
Whether you’re building a microservices architecture from scratch or looking to optimize inter-service communication in an existing Node.js ecosystem, this guide will help you understand how gRPC works under the hood, and how to set up two Node.js servers to talk to each other using gRPC.
💡 Note: If you’re completely new to gRPC, we recommend first checking out our introductory blog on gRPC where we break down its core concepts, benefits over REST, and real-world use cases.
gRPC Communication Flow: What Happens Under the Hood
When a gRPC client calls a remote service, the whole process looks like this:
- Define a .proto file (Protocol Buffers schema)
- The first step in any gRPC-based system is defining the structure of the request, response, and available services using a .proto file. This file acts as a contract between the client and server.
- It contains message types and service definitions, ensuring both sides agree on the structure of communication, even if they’re written in different programming languages.
- Generate code (or load dynamically)
- Once the .proto file is written, it needs to be used in your code. This can be done in two ways:
- Either by generating JavaScript files using grpc-tools (which compiles the .proto into binary-safe code),
- Or by loading the .proto dynamically at runtime using @grpc/proto-loader.
- In both cases, the end result is a JavaScript object representation of your services and messages, enabling actual code execution.
- Once the .proto file is written, it needs to be used in your code. This can be done in two ways:
- Client calls a method → data converted to binary
- When a client calls a gRPC method, it passes JavaScript object data, but under the hood, this object is converted into a binary format using Protocol Buffers (Protobuf).
- Binary request sent over network (HTTP/2)
- The binary-encoded data is transmitted from the client to the server over HTTP/2, which provides features like multiplexing, low latency, and persistent connections.
- Server receives binary → converts to usable object
- Once the server receives the binary request, it uses the same .proto schema (either generated or loaded) to decode the binary data back into a usable JavaScript object.
- This ensures that the structure of the message is exactly what was expected, maintaining type safety and message integrity.
- Server processes and sends back response → also as binary
- After decoding the request and performing the necessary logic , the server builds a response object and encodes it back into binary format using Protobuf.
- This binary response is then sent over the network back to the client using HTTP/2, just like the incoming request.
- Client receives binary response → decoded back into object
- Finally, the client receives the binary response and decodes it back into a JavaScript object using the same .proto definition. This decoded object can then be used in the application — to show data in the UI, log responses, or trigger other business logic.
Tools Used to Handle .proto File
There are two main ways to work with .proto files in Node.js:
Tool | How it works | When to use |
@grpc/proto-loader | Loads .proto at runtime, creates dynamic objects | Good for flexibility and quick setup |
grpc-tools | Compiles .proto into .js and .ts files | Better for performance, type safety, and production use |
NOTE:- In this guide, we’ll use grpc-tools, which converts .proto into ready-to-use JS classes and service definitions before runtime.
Node.js gRPC Client-Server Communication Using proto Tool
Below is a complete working example of two separate Node.js projects — one acting as a gRPC Server and the other as a gRPC Client. The .proto file is compiled into JavaScript using grpc-tools, enabling strongly typed and high-performance communication.
- Folder Structure
Create two node js project with below folder structure
- Common .proto File
📁 proto/user.proto (same in both projects)
syntax = "proto3";
package user;
service UserService {
rpc GetUserDetails (UserRequest) returns (UserResponse);
}
message UserRequest {
string userId = 1;
string authToken = 2;
}
message UserResponse {
string name = 1;
string email = 2;
string status = 3;
}
- Setup in Server Project
- 📁 grpc-server/generate.js
const path = require('path');
const { execSync } = require('child_process');
const protoPath = path.resolve(__dirname, 'proto', 'user.proto');
const outDir = path.resolve(__dirname, 'generated');
execSync(`
npx grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:${outDir} \
--grpc_out=grpc_js:${outDir} \
--proto_path=${path.dirname(protoPath)} \
${protoPath}
`);
console.log('gRPC code generated (Server)');
3. 📁 grpc-server/index.js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');
const userProto = require('./generated/user_pb');
const service = require('./generated/user_grpc_pb');
function GetUserDetails(call, callback) {
const request = call.request;
const userId = request.getUserid();
const authToken = request.getAuthtoken();
const response = new userProto.UserResponse();
if (authToken !== 'valid-token') {
response.setName('');
response.setEmail('');
response.setStatus('Unauthorized');
return callback(null, response);
}
response.setName('John Doe');
response.setEmail(`${userId}@example.com`);
response.setStatus('Success');
callback(null, response);
}
function main() {
const server = new grpc.Server();
server.addService(service.UserServiceService, { getUserDetails: GetUserDetails });
const address = '0.0.0.0:50051';
server.bindAsync(address, grpc.ServerCredentials.createInsecure(), () => {
console.log(`gRPC Server running at ${address}`);
server.start();
});
}
main();
4.Setup in Client Project
- 📁 grpc-client/generate.js
const path = require('path');
const { execSync } = require('child_process');
const protoPath = path.resolve(__dirname, 'proto', 'user.proto');
const outDir = path.resolve(__dirname, 'generated');
execSync(`
npx grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:${outDir} \
--grpc_out=grpc_js:${outDir} \
--proto_path=${path.dirname(protoPath)} \
${protoPath}
`);
console.log('gRPC code generated (Client)');
📁 grpc-client/index.js
const grpc = require('@grpc/grpc-js');
const userProto = require('./generated/user_pb');
const service = require('./generated/user_grpc_pb');
const client = new service.UserServiceClient(
'localhost:50051',
grpc.credentials.createInsecure()
);
function main() {
const request = new userProto.UserRequest();
request.setUserid('john123');
request.setAuthtoken('valid-token');
client.getUserDetails(request, (err, response) => {
if (err) {
console.error('Error:', err);
} else {
console.log('Response:');
console.log('Name:', response.getName());
console.log('Email:', response.getEmail());
console.log('Status:', response.getStatus());
}
});
}
main();
5. ▶️ How to Run
- Install following dependencies, in each project i.e. in both grpc-client & grpc-server
npm install @grpc/grpc-js grpc-tools grpc_tools_node_protoc_ts
- Compile .proto to JS Files Using grpc-tools i.e. in both grpc-client & grpc-server, using below command
node generate.js
- This generates user_pb.js and user_grpc_pb.js in both grpc-server/ and grpc-client/
- user_pb.js: This file contains the structure of the data you send and receive (like templates for your messages).
- user_grpc_pb.js: This file contains the code needed for the client and server to talk to each other (like a menu of available services).
- You don’t write these files yourself — the system generates them so your app can understand and follow the rules defined in the .proto file.
- Now start server first
node index.js
- Now use index.js file in grpc-client to make a grpc call to server
node index.js
As soon as you execute above command, you will get below output
Response:
Name: John Doe
Email: john123@example.com
Status: Success
Conclusion
- gRPC brings powerful, efficient, and scalable communication to modern applications — especially microservices. With binary-level data transmission, HTTP/2 support, and strongly typed .proto contracts, it ensures both speed and safety across services.
- Whether you choose to generate static code using grpc-tools or load .proto dynamically at runtime using @grpc/proto-loader, gRPC gives you flexibility along with performance.
- By now, you’ve seen the complete journey — from writing a .proto file, understanding how messages convert to binary, to setting up real communication between two Node.js services. You’re all set to start using gRPC in your own projects!