Building Real-Time Apps with gRPC: A Complete Guide Using Node.js and Redis
What are gRPCs?
gRPC is an open-source framework used for service-to-service communication developed by Google. The abbreviation RPC in its name, Remote Procedure Call, indicates that we can call a method on a remote server as if it were our own method. It is based around the idea of defining a service, specifying the methods that can be called remotely with their parameters and return types. On the server side, the server implements this interface and runs a gRPC server to handle client calls. On the client side, the client has a stub (referred to as just a client in some languages) that provides the same methods as the server.
Why We Needed gRPC ?
Before gRPC, microservices typically communicated using HTTP/1.1, often with JSON as the payload format for RESTful APIs. While effective, this approach had limitations. JSON’s text-based format results in larger payloads and slower serialization/deserialization compared to binary formats. Additionally, HTTP/1.1’s sequential request-response model can introduce latency due to head-of-line blocking, and real-time communication often required complex solutions like WebSockets for streaming use cases. This is where gRPC shines. Developed by Google, gRPC addresses these issues by offering a high-performance RPC framework that delivers smaller, faster payloads, low-latency communication, and native support for real-time streaming. This makes gRPC ideal for microservices, distributed systems, and applications requiring efficient, scalable communication.
How gRPC's works under the hood?
1 A small flow diagram
2[Client Code]
3 ↓
4[Client Stub] ← auto-generated
5 ↓
6[Protobuf Serialization]
7 ↓
8[HTTP/2 Transport]
9 ↓
10[Server Stub] ← auto-generated
11 ↓
12[Service Implementation]
13gRPC works by having a client stub serialize a method call into a Protocol Buffers message, sent over an HTTP/2 connection with headers and metadata. The server deserializes the message, executes the service logic, and serializes the response back to the client. HTTP/2 enables efficient, multiplexed, and streaming communication, with the connection reused for low latency.
Using gRPC with Node.js
Let’s apply what we’ve learned by building a small implementation that demonstrates gRPC’s messaging and streaming capabilities. What are we building? We will create a simple application that allows users to send messages with their username and receive a stream of messages based on a subscribed room, using Server-Sent Events (SSE) for real-time notifications. Project Structure: We will create four repositories: A simple client using React A proxy server to act as a bridge between the client and other services A Redis server for message broadcasting A gRPC server to handle messaging and streaming logic

chat.proto
1syntax = "proto3";
2
3service ChatService {
4 rpc SendMessage (MessageRequest) returns (MessageResponse);
5 rpc StreamNotifications (NotificationRequest) returns (stream NotificationResponse);
6}
7
8message MessageRequest {
9 string user = 1;
10 string content = 2;
11}
12
13message MessageResponse {
14 string status = 1;
15}
16
17message NotificationRequest {
18 string user = 1;
19}
20
21message NotificationResponse {
22 string message = 1;
23}
24So this chat.proto file defines the gRPC service and structure using Protocol Buffers.
It specifies a unary SendMessage RPC for sending chat messages and a server-streaming StreamNotifications RPC for real-time notifications. It includes message structures like MessageRequest (with user and content) and NotificationResponse (with message), which are serialized into a compact binary format for efficient HTTP/2 transport.
grpc.server
1const PROTO_PATH = path.join(__dirname, 'proto/chat.proto');
2const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
3 keepCase: true,
4 longs: String,
5 enums: String,
6 defaults: true,
7 oneofs: true
8});
9const chatProto = grpc.loadPackageDefinition(packageDefinition).ChatService;
10
11// gRPC service implementation
12const sendMessage = async (call, callback) => {
13 const { user, content } = call.request;
14 console.log(`Received message from ${user}: ${content}`);
15
16 await redisClient.publish('chat_channel', `${user}: ${content}`);
17
18 callback(null, { status: 'Message received and published' });
19};
20const streamNotifications = (call) => {
21 const { user } = call.request;
22 const subscriber = redis.createClient({ url: 'redis://localhost:6379' });
23 subscriber.connect();
24 subscriber.subscribe('chat_channel', (message) => {
25 if (message.startsWith(`${user}:`)) {
26 call.write({ message });
27 }
28 });
29 call.on('cancelled', () => {
30 subscriber.quit();
31 });
32};
33// Start gRPC server
34const server = new grpc.Server();
35server.addService(chatProto.service, {
36 SendMessage: sendMessage,
37 StreamNotifications: streamNotifications
38});
39
40server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
41 console.log('gRPC server running on port 50051');
42 server.start();
43});
44The grpcServer.js file implements the gRPC server for the ChatService defined in chat.proto. It handles the unary SendMessage RPC by deserializing the client’s MessageRequest, publishing the message to a Redis channel, and returning a MessageResponse. For the server-streaming StreamNotifications RPC, it subscribes to Redis, filters messages for the specified user, and streams NotificationResponse messages over an HTTP/2 connection.
grpc client proxy
1const PROTO_PATH = path.join(__dirname, '../grpc-server/proto/chat.proto');
2const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
3 keepCase: true,
4 longs: String,
5 enums: String,
6 defaults: true,
7 oneofs: true
8});
9const ChatService = grpc.loadPackageDefinition(packageDefinition).ChatService;
10
11const client = new ChatService('localhost:50051', grpc.credentials.createInsecure());
12
13console.log('Testing gRPC connection...');
14client.SendMessage({ user: 'test', content: 'hello' }, (err, response) => {
15 if (err) console.error('Connection Test Error:', err);
16 else console.log('Connection Test Success:', response);
17});
18
19module.exports = {
20 sendMessage: (user, content, callback) => {
21 client.SendMessage({ user, content }, (err, response) => {
22 callback(err, response);
23 });
24 },
25 streamNotifications: (user, onData) => {
26 if (!user || user.trim() === '') {
27 console.log('Invalid user for stream');
28 return { cancel: () => { } };
29 }
30 const call = client.StreamNotifications({ user });
31 call.on('data', (data) => {
32 onData(data);
33 });
34 call.on('end', () => {
35 console.log('Stream ended');
36 });
37 call.on('error', (err) => {
38 if (err.code === grpc.status.CANCELLED) {
39 console.log('Stream cancelled by client');
40 } else {
41 console.error('Stream error:', err);
42 }
43 });
44 return call;
45 }
46};
47The grpcClient.js file sets up a gRPC client to interact with the ChatService defined in chat.proto. It loads the Protobuf definition, creates a client connected to the gRPC server at localhost:50051, and exports two functions: sendMessage to send chat messages, and streamNotifications to initiate the server-streaming StreamNotifications. This client is used by the Express server to relay messages and streams between the React client and the gRPC backend.
proxy server.js
1app.use(express.json());
2
3// REST endpoint to send messages
4app.post('/send', (req, res) => {
5 const { user, content } = req.body;
6 if (!user || !content) {
7 return res.status(400).json({ error: 'User and content are required' });
8 }
9 sendMessage(user, content, (err, response) => {
10 if (err) {
11 console.error('SendMessage error:', err);
12 return res.status(500).json({ error: err.message });
13 }
14 console.log(`Sent message for user: ${user}`);
15 res.json({ status: response.status });
16 });
17});
18
19// Server-Sent Events for streaming notifications
20app.get('/notifications/:user', (req, res) => {
21 const user = req.params.user;
22 if (!user || user.trim() === '') {
23 console.log('Invalid user parameter');
24 return res.status(400).send('User parameter is required');
25 }
26 console.log(`Starting SSE for user: ${user}`);
27 res.setHeader('Content-Type', 'text/event-stream');
28 res.setHeader('Cache-Control', 'no-cache');
29 res.setHeader('Connection', 'keep-alive');
30
31 const stream = streamNotifications(user, (data) => {
32 console.log(`Sending notification to user ${user}: ${JSON.stringify(data)}`);
33 res.write(`data: ${JSON.stringify(data)}\n\n`);
34 });
35
36 req.on('close', () => {
37 console.log(`SSE connection closed for user: ${user}`);
38 stream.cancel();
39 });
40
41 // Keep SSE alive with periodic pings
42 const keepAlive = setInterval(() => {
43 res.write(': ping\n\n');
44 }, 15000);
45
46 req.on('close', () => {
47 clearInterval(keepAlive);
48 });
49});
50
51app.listen(3001, () => console.log('API server running on port 3001'));
52The file runs an Express proxy that connects a React client to the gRPC backend. Its /send endpoint triggers the unary SendMessage RPC, forwarding the client’s message to the gRPC server and returning the response as JSON. The /notifications/:user endpoint uses Server-Sent Events (SSE) to stream NotificationResponse messages from the gRPC StreamNotifications RPC, converting HTTP/2 streams into browser-compatible real-time updates.
These were the main files that handle all the core tasks. For the complete application, you can check out the GitHub repository here: Click here
I believe I’ve covered all the essential concepts needed to understand gRPC and how to use it with Node.js.
As a personal remark — based on my experience — gRPC is genuinely fun to work with, especially when it comes to scalability. It can truly be a game changer in terms of performance and efficiency for modern backend systems. !
Comments
Please log in to post a comment.
No comments yet. Be the first to comment!