gRPC Explained: APIs That Speak in Binary
A shop dashboard needs one thing from the orders service: the details of order 1042. Over plain JSON the request is easy to write and easy to break. A field is renamed from customer_name to customerName, the dashboard still reads the old name, and the page shows a blank. Nothing errors, and it surfaces only when a customer asks where their parcel is. Server and client each hold a private idea of what an order looks like, and nothing checks that the two still match. gRPC closes exactly that gap: both sides share one contract, a .proto file, and the code on each side is generated from it, so a renamed field breaks the build instead of the page.
What gRPC actually is
A gRPC service starts with a contract, not with code. The contract is a .proto file that names the service, its methods, and the shape of every message, using Protocol Buffers, a binary format. A tool reads that file and generates the client and server code in your language, so both sides agree on the shape of the data by construction. The calls travel over HTTP/2, which lets many calls share one connection, and the messages travel as compact binary instead of text, so they are smaller and faster to parse than JSON. gRPC also supports streaming in both directions: a server can send results as they arrive, a client can send a stream of requests, or both can talk at once over a single call. That is the whole idea. One shared contract, generated code, binary messages, and HTTP/2 underneath.
What I would check before using it
My judgement, from years of building small services: gRPC earns its place when two services you own talk to each other a lot, and it is the wrong tool when the other side is a browser or a stranger. The first check is who owns both ends. If your team writes server and client, the shared .proto file is a real contract: a change to it breaks the build on both sides at once. If the client belongs to a partner or a public developer, that contract becomes a burden, because they must learn your tooling first. A browser cannot speak gRPC at all, so a public or browser-facing API should stay JSON over HTTP.
The second check is how chatty the calls are. gRPC runs on HTTP/2, so one connection carries many calls at once, and it supports streaming in both directions, where the server sends results as they arrive instead of making the client wait for one big answer. A dashboard watching order status change in real time is a natural fit. One request returning a single order, once a minute, gains almost nothing from that machinery.
The third check is the tooling. Debugging a JSON API means opening a URL in a browser or running curl. Debugging gRPC means reaching for a tool like grpcurl, and your load balancer and proxies must understand HTTP/2. That is manageable, but it is real work to set up before the first service ships.
The verdict: use gRPC for the shop's services, where the team owns both ends and the calls are frequent, and keep JSON for anything public or browser-facing. The recommendation flips the moment the API has to serve someone else's code. Then the simplicity of a JSON endpoint beats the safety of a shared contract.
An orders service, end to end
The worked example is the shop from the opening: an orders service that looks up one order by id, with real order data, a server, and a client that calls it. The contract comes first, in orders.proto:
syntax = "proto3";
package shop;
service Orders {
rpc GetOrder (OrderRequest) returns (Order);
}
message OrderRequest {
int32 id = 1;
}
message Order {
int32 id = 1;
string customer_name = 2;
string item = 3;
int32 quantity = 4;
double unit_price = 5;
bool paid = 6;
string status = 7;
}
The numbers beside each field are tags, not values. They identify the field in the binary message, so a field can be renamed without breaking old clients, but a tag must never be reused for a different field. The server loads this contract, keeps two real orders in memory, and answers GetOrder, returning a NOT_FOUND error for an unknown id:
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync('orders.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const shop = grpc.loadPackageDefinition(packageDefinition).shop;
const ORDERS = {
1042: {
id: 1042,
customer_name: 'Ayesha',
item: 'Wireless headphones',
quantity: 2,
unit_price: 59.99,
paid: true,
status: 'ready to ship',
},
1043: {
id: 1043,
customer_name: 'Bilal',
item: 'USB-C cable',
quantity: 5,
unit_price: 8.5,
paid: false,
status: 'awaiting payment',
},
};
function getOrder(call, callback) {
const order = ORDERS[call.request.id];
if (!order) {
return callback({
code: grpc.status.NOT_FOUND,
details: 'Order ' + call.request.id + ' does not exist',
});
}
callback(null, order);
}
const server = new grpc.Server();
server.addService(shop.Orders.service, { GetOrder: getOrder });
server.bindAsync(
'127.0.0.1:50051',
grpc.ServerCredentials.createInsecure(),
(err, port) => {
if (err) {
console.error('Server failed to start:', err.message);
return;
}
console.log('Orders service listening on 127.0.0.1:' + port);
}
);
The client loads the same contract, connects, and asks for order 1042, then works out the total from the quantity and the unit price:
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync('orders.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const shop = grpc.loadPackageDefinition(packageDefinition).shop;
const target = process.argv[2] || '127.0.0.1:50051';
const orderId = Number(process.argv[3] || 1042);
const client = new shop.Orders(target, grpc.credentials.createInsecure());
client.GetOrder({ id: orderId }, (err, order) => {
if (err) {
console.log('Call failed:', err.code, err.details);
return;
}
const total = (order.quantity * order.unit_price).toFixed(2);
console.log('Order ' + order.id + ' for ' + order.customer_name + ':');
console.log(' ' + order.quantity + ' x ' + order.item + ' at $' + order.unit_price);
console.log(' Total: $' + total);
console.log(' Paid: ' + order.paid + ', status: ' + order.status);
});
Starting the server and running the client gave this real output:
Orders service listening on 127.0.0.1:50051
Order 1042 for Ayesha:
2 x Wireless headphones at $59.99
Total: $119.98
Paid: true, status: ready to ship
Order 1043 works the same way, and shows the unpaid case:
Order 1043 for Bilal:
5 x USB-C cable at $8.5
Total: $42.50
Paid: false, status: awaiting payment
Now the part the tidy tutorials skip: something goes wrong. The client is pointed at port 50052, where nothing is listening, because the port number was typed wrong. The call fails before it even reaches a server:
Call failed: 14 No connection established. Last error: Error: connect ECONNREFUSED 127.0.0.1:50052. Resolution note:
Error 14 is the gRPC code for UNAVAILABLE, and ECONNREFUSED means nothing answered on that port. The fix is to check the port the server actually bound to, which it prints when it starts, and to point the client at the same one. Running the client against 127.0.0.1:50051 again gives the order back at once. The lesson is that a gRPC failure usually has nothing to do with your message. Check the address and the port first, then check whether the server is running at all, and only then start doubting the code.
Common mistakes and how to fix them
1. Connecting to the wrong port, or to a server that is not running. The symptom is Call failed: 14 No connection established with ECONNREFUSED. The fix is to confirm the server printed its listening line and the client's target matches it.
2. Calling a method that does not exist. Writing client.GetOrders, with an extra s, when the contract defines GetOrder, gives TypeError: client.GetOrders is not a function. Method names come from the .proto file, so check the spelling against the contract and reload the definition after editing it.
3. Reading a field with the wrong case. With keepCase: true the field arrives as customer_name, and reading order.customerName silently returns undefined. Use the same loader options on client and server, and read fields as the loader names them.
4. Asking for an order that does not exist, without handling the error. Calling GetOrder with id 9999 gives Call failed: 5 Order 9999 does not exist, because the server returns NOT_FOUND, code 5. A client that ignores the err argument will crash on the next line when it reads a field of an undefined order. Check err first in every callback, and branch on err.code for different responses.
What to do next
gRPC is worth the setup when the contract is the point: services you own, talking often, where a renamed field should break the build and never a customer's page. The binary messages are a bonus; the reason is that the .proto file makes the agreement between two services visible, versioned, and checked by the compiler. The next step is to take one internal call in a project of your own, write its .proto file, and run server and client on your own machine. When a wrong port gives you error 14 and the first correct call prints a real order, you will know whether your next service deserves a contract or just a URL.
Comments
Post a Comment