gRPC with Node.js — Complete Guide 2026
Advertisement
Introduction
Why This Matters
gRPC is the preferred communication protocol for internal microservices at Google, Netflix, and Uber. It is up to 7x faster than REST over JSON because it uses binary Protocol Buffers and HTTP/2 multiplexing. If you are building high-throughput service-to-service communication in 2026, gRPC is the standard to know.
What is gRPC?
gRPC is a Remote Procedure Call framework developed by Google. It uses Protocol Buffers (protobuf) as its Interface Definition Language and serialization format, and HTTP/2 as its transport layer.
Key advantages:
- Binary serialization is 5-10x smaller than JSON
- HTTP/2 multiplexing eliminates head-of-line blocking
- Built-in support for four communication patterns: unary, server streaming, client streaming, and bidirectional streaming
- Code generation for 10+ languages from a single
.protofile - Built-in deadline propagation and cancellation
npm install @grpc/grpc-js @grpc/proto-loader
npm install --save-dev grpc-tools grpc_tools_node_protoc_tsDefining a Service with Protocol Buffers
The .proto file is the source of truth. It defines your service contract in a language-agnostic way.
// proto/user.proto
syntax = "proto3";
package user;
service UserService {
// Unary RPC
rpc GetUser (GetUserRequest) returns (UserResponse);
// Server streaming
rpc ListUsers (ListUsersRequest) returns (stream UserResponse);
// Client streaming
rpc CreateUsers (stream CreateUserRequest) returns (BatchCreateResponse);
// Bidirectional streaming
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
message GetUserRequest {
string id = 1;
}
message CreateUserRequest {
string email = 1;
string name = 2;
}
message UserResponse {
string id = 1;
string email = 2;
string name = 3;
}
message ListUsersRequest {
int32 page_size = 1;
}
message BatchCreateResponse {
int32 created = 1;
}
message ChatMessage {
string from = 1;
string message = 2;
}Building the gRPC Server in TypeScript
// src/server.ts
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import path from 'path';
const PROTO_PATH = path.join(__dirname, '../proto/user.proto');
const packageDef = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const proto = grpc.loadPackageDefinition(packageDef) as any;
// Unary RPC handler
async function getUser(
call: grpc.ServerUnaryCall<{ id: string }, unknown>,
callback: grpc.sendUnaryData<unknown>
) {
const user = await db.users.findById(call.request.id);
if (!user) {
return callback({
code: grpc.status.NOT_FOUND,
message: `User ${call.request.id} not found`,
});
}
callback(null, user);
}
// Server streaming handler
function listUsers(call: grpc.ServerWritableStream<{ page_size: number }, unknown>) {
const users = db.users.findAll();
for (const user of users) {
call.write(user);
}
call.end();
}
// Client streaming handler
async function createUsers(
call: grpc.ServerReadableStream<{ email: string; name: string }, unknown>,
callback: grpc.sendUnaryData<unknown>
) {
let created = 0;
call.on('data', async (req: { email: string; name: string }) => {
await db.users.create(req);
created++;
});
call.on('end', () => {
callback(null, { created });
});
}
const server = new grpc.Server();
server.addService(proto.user.UserService.service, {
getUser,
listUsers,
createUsers,
});
server.bindAsync(
'0.0.0.0:50051',
grpc.ServerCredentials.createInsecure(),
(err, port) => {
if (err) throw err;
console.log(`gRPC server running on port ${port}`);
}
);Building the gRPC Client
// src/client.ts
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import path from 'path';
const PROTO_PATH = path.join(__dirname, '../proto/user.proto');
const packageDef = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const proto = grpc.loadPackageDefinition(packageDef) as any;
const client = new proto.user.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);
// Unary call — promisified
function getUserById(id: string): Promise<unknown> {
return new Promise((resolve, reject) => {
client.getUser({ id }, (err: Error | null, response: unknown) => {
if (err) return reject(err);
resolve(response);
});
});
}
// Server streaming call
function streamUsers(): void {
const stream = client.listUsers({ page_size: 100 });
stream.on('data', (user: unknown) => {
console.log('Received user:', user);
});
stream.on('end', () => {
console.log('Stream complete');
});
stream.on('error', (err: Error) => {
console.error('Stream error:', err);
});
}
// Usage
const user = await getUserById('user-123');
streamUsers();Deadlines and Metadata
One of gRPC's most powerful features is deadline propagation — timeouts are passed through the entire call chain.
// Set a 5-second deadline on any call
const deadline = new Date();
deadline.setSeconds(deadline.getSeconds() + 5);
client.getUser(
{ id: 'user-123' },
{ deadline },
(err: Error | null, response: unknown) => {
if (err?.message.includes('DEADLINE_EXCEEDED')) {
console.error('Service took too long');
}
}
);
// Adding metadata (like auth tokens)
const metadata = new grpc.Metadata();
metadata.add('authorization', `Bearer ${token}`);
client.getUser({ id: 'user-123' }, metadata, callback);TLS and Production Security
Never run gRPC without TLS in production. Use channel credentials to encrypt the connection.
import fs from 'fs';
// Server with TLS
const serverCredentials = grpc.ServerCredentials.createSsl(
fs.readFileSync('certs/ca.crt'),
[{
cert_chain: fs.readFileSync('certs/server.crt'),
private_key: fs.readFileSync('certs/server.key'),
}],
true // require client certs (mTLS)
);
server.bindAsync('0.0.0.0:50051', serverCredentials, callback);
// Client with TLS
const channelCredentials = grpc.credentials.createSsl(
fs.readFileSync('certs/ca.crt'),
fs.readFileSync('certs/client.key'),
fs.readFileSync('certs/client.crt')
);
const secureClient = new proto.user.UserService('service:50051', channelCredentials);Common Mistakes
Mistake 1 — Using insecure credentials in production: always enable TLS and ideally mutual TLS (mTLS) for service-to-service calls.
Mistake 2 — Not setting deadlines: without a deadline, a stuck downstream service will hold your goroutine/thread forever.
Mistake 3 — Treating gRPC like REST: do not use HTTP status codes in error messages — use gRPC status codes (NOT_FOUND, INVALID_ARGUMENT, INTERNAL).
Mistake 4 — Ignoring back-pressure on streaming: when writing to a server stream faster than the client can read, buffer memory grows unboundedly. Check call.write() return value.
Best Practices
- Generate TypeScript types from
.protofiles usinggrpc_tools_node_protoc_ts— never write types by hand. - Keep
.protofiles in a shared package or Git submodule so client and server stay in sync. - Use gRPC interceptors (middleware) for logging, authentication, and tracing instead of duplicating logic in every handler.
- Always set deadlines on outbound calls; propagate the incoming deadline from parent calls to child calls.
- Use
grpc.status.NOT_FOUND,grpc.status.UNAUTHENTICATED, etc. — structured errors are machine-readable by clients.
Key Takeaways
- gRPC uses Protocol Buffers and HTTP/2, making it 5-10x more bandwidth-efficient than REST over JSON.
- A single
.protofile generates type-safe client and server code in 10+ languages. - gRPC supports four communication patterns: unary, server streaming, client streaming, and bidirectional streaming.
- Deadlines propagate through the call chain automatically, enabling distributed timeout control.
- mTLS (mutual TLS) is the recommended security model for internal service-to-service gRPC communication.
- gRPC status codes (
NOT_FOUND,DEADLINE_EXCEEDED) are the correct error mechanism — not HTTP status codes. - gRPC is best suited for internal microservice communication; for public APIs, REST or GraphQL is more accessible.
- Use interceptors for cross-cutting concerns like auth and logging rather than embedding them in handler functions.
Advertisement