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] 34[Client Stub] ← auto-generated 56[Protobuf Serialization] 78[HTTP/2 Transport] 910[Server Stub] ← auto-generated 1112[Service Implementation] 13

gRPC 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} 24

So 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}); 44

The 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}; 47

The 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')); 52

The 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. !

- Meet Jain

Comments

Please log in to post a comment.

No comments yet. Be the first to comment!