In typical web development, we use REST APIs to allow the client (like a browser or mobile app) to talk to the server. It’s great for public APIs, CRUD apps, and simple systems.
But what about when servers need to talk to other servers?
That’s where RPC (Remote Procedure Call) comes in. It’s more direct and efficient way for services (like microservices) to communicate internally.
REST = great for frontend ↔ backend
RPC/gRPC = great for backend ↔ backend (especially in microservices)
In this post, we’ll break down:
- What RPC and gRPC are
- Why gRPC is becoming the go-to solution
- A real-world example using Node.js and Next.js
Let’s dive in.
What is RPC?
RPC (Remote Procedure Call) is a way for programs to call functions on another computer as if they were local.
For example:
const result = remoteService.getUserData("abc123");
Looks like a local call — but the logic might be running on a server across the country. RPC hides the networking details from you. That’s both the strength and the trap.
What is gRPC?
gRPC, built by Google, is a modern take on RPC that’s:
- Fast (uses HTTP/2 and binary encoding)
- Language-agnostic
- Automatically code-generating
- Great for service-to-service communication
gRPC replaces typical JSON-over-HTTP REST calls with something more structured, type-safe, and efficient — ideal for microservices.
RPC vs gRPC at a Glance
| Feature | Classic RPC / REST | gRPC |
|---|---|---|
| Transport | HTTP 1.1 | HTTP/2 |
| Data Format | JSON / XML | Protocol Buffers (Protobuf) |
| Code Generation | Manual or OpenAPI | Automatic via protoc |
| Streaming | Not common | Fully supported (all directions) |
| Language Support | Varies | Wide (Go, Node.js, Python, etc.) |
| Speed | Slower, larger payloads | Faster, compact binary messages |
Why Use gRPC?
Here’s why gRPC is gaining traction in modern backend systems:
Better performance: Protobufs are smaller and faster than JSON
Strict contracts: Protobuf definitions define the shape of all messages
Easy code sharing: gRPC generates client + server code from the same .proto file
Streaming support: Perfect for real-time data and efficient large-scale pipelines
Cross-language: Use gRPC with Go, Rust, Python, Java, or anything else
Real-World Example: Node.js gRPC Server + Next.js Client
Let’s build a basic app where:
- A Node.js gRPC server manages orders
- A Next.js frontend requests order status from that server
Project Structure
grpc-demo/
├── grpc-server/
│ └── server.js
├── proto/
│ └── order.proto
├── next-app/
│ └── pages/api/order.js
1. Define the Protobuf Contract
File: proto/order.proto
syntax = "proto3";
package order;
service OrderService {
rpc GetOrderStatus (OrderRequest) returns (OrderResponse);
}
message OrderRequest {
string orderId = 1;
}
message OrderResponse {
string status = 1;
}
2. gRPC Server in Node.js
File: grpc-server/server.js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');
const PROTO_PATH = path.join(__dirname, '../proto/order.proto');
const packageDef = protoLoader.loadSync(PROTO_PATH);
const orderProto = grpc.loadPackageDefinition(packageDef).order;
function GetOrderStatus(call, callback) {
const { orderId } = call.request;
console.log(`Received request for order ${orderId}`);
callback(null, { status: "Processing" });
}
function main() {
const server = new grpc.Server();
server.addService(orderProto.OrderService.service, { GetOrderStatus });
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
console.log("gRPC server running on port 50051");
server.start();
});
}
main();
Install dependencies:
npm install @grpc/grpc-js @grpc/proto-loader
Run it:
node grpc-server/server.js
3. Next.js Client API Route
File: next-app/pages/api/order.js
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import path from 'path';
const PROTO_PATH = path.resolve('./proto/order.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const orderProto = grpc.loadPackageDefinition(packageDefinition).order;
const client = new orderProto.OrderService(
'localhost:50051',
grpc.credentials.createInsecure()
);
export default function handler(req, res) {
const { orderId } = req.query;
client.GetOrderStatus({ orderId }, (err, response) => {
if (err) {
return res.status(500).json({ error: err.message });
}
res.status(200).json({ status: response.status });
});
}
Test it in your browser:
http://localhost:3000/api/order?orderId=xyz123
You’ll get:
{
"status": "Processing"
}
Let go though some terminology we just learnt.
Protobuf
Instead of using JSON, gRPC uses Protocol Buffers for defining and serializing messages.
Example Protobuf message:
message User {
string name = 1;
int32 age = 2;
}
Compared to JSON:
{ "name": "Alice", "age": 30 }
The Protobuf version is smaller, faster to transmit, and type-safe.
with this we can uset this for building bidirectional streams, NestJS integration, or JWT-secured gRPC APIs.