gRPC Complete Guide | Protocol Buffers, Streaming, Node.js & TypeScript
이 글의 핵심
gRPC is 5-10x faster than REST for service-to-service communication — binary Protocol Buffers, HTTP/2 multiplexing, and strongly typed contracts. This guide covers proto schema design, all four streaming patterns, and full Node.js TypeScript implementation.
gRPC vs REST
REST (JSON over HTTP/1.1):
POST /users Content-Type: application/json
Body: {"name":"Alice","email":"[email protected]"}
→ JSON parsing, no schema enforcement, HTTP/1.1 per-request connection
gRPC (Protobuf over HTTP/2):
rpc CreateUser(CreateUserRequest) returns (User)
→ Binary serialization, schema-enforced, HTTP/2 multiplexed streams
→ 5-10x smaller payload, 5-10x faster serialization
When to use gRPC:
- Service-to-service communication (microservices)
- High-throughput internal APIs
- Real-time bidirectional streaming
- Polyglot environments (client in Go, server in Node.js)
Setup
npm install @grpc/grpc-js @grpc/proto-loader
npm install -D grpc-tools grpc_tools_node_protoc_ts typescript ts-node
Protocol Buffer Schema
// proto/user.proto
syntax = "proto3";
package user;
// Message definitions
message User {
int32 id = 1;
string name = 2;
string email = 3;
Role role = 4;
int64 created_at = 5; // Unix timestamp
}
enum Role {
VIEWER = 0; // First value must be 0 in proto3
EDITOR = 1;
ADMIN = 2;
}
message CreateUserRequest {
string name = 1;
string email = 2;
Role role = 3;
}
message GetUserRequest {
int32 id = 1;
}
message ListUsersRequest {
int32 page = 1;
int32 page_size = 2;
}
message ListUsersResponse {
repeated User users = 1;
int32 total_count = 2;
bool has_next_page = 3;
}
message DeleteUserRequest {
int32 id = 1;
}
message Empty {}
// Service definition — the API contract
service UserService {
// Unary RPC
rpc CreateUser(CreateUserRequest) returns (User);
rpc GetUser(GetUserRequest) returns (User);
rpc DeleteUser(DeleteUserRequest) returns (Empty);
// Server streaming — server sends a stream of responses
rpc ListUsers(ListUsersRequest) returns (stream User);
// Client streaming — client sends a stream of requests
rpc BatchCreateUsers(stream CreateUserRequest) returns (ListUsersResponse);
// Bidirectional streaming
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message ChatMessage {
string user_id = 1;
string text = 2;
int64 timestamp = 3;
}
Code Generation
# Generate TypeScript types and JS stubs from .proto
protoc \
--plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \
--js_out=import_style=commonjs,binary:./src/generated \
--ts_out=./src/generated \
--grpc_out=grpc_js:./src/generated \
--proto_path=./proto \
proto/user.proto
Or use buf (modern protobuf toolchain):
npm install -D @bufbuild/buf @bufbuild/protoc-gen-es @connectrpc/protoc-gen-connect-es
# buf.gen.yaml
version: v1
plugins:
- plugin: es
out: src/generated
- plugin: connect-es
out: src/generated
buf generate
Server Implementation
// src/server.ts
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import path from 'path';
// Load proto definition
const packageDef = protoLoader.loadSync(
path.join(__dirname, '../proto/user.proto'),
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
}
);
const proto = grpc.loadPackageDefinition(packageDef) as any;
// In-memory store (replace with real DB)
const users = new Map<number, any>();
let nextId = 1;
// Service implementation
const userService = {
// Unary RPC
createUser(
call: grpc.ServerUnaryCall<any, any>,
callback: grpc.sendUnaryData<any>
) {
const { name, email, role } = call.request;
if (!name || !email) {
return callback({
code: grpc.status.INVALID_ARGUMENT,
message: 'Name and email are required',
});
}
const user = {
id: nextId++,
name,
email,
role: role || 'VIEWER',
created_at: Date.now(),
};
users.set(user.id, user);
callback(null, user);
},
getUser(
call: grpc.ServerUnaryCall<any, any>,
callback: grpc.sendUnaryData<any>
) {
const user = users.get(call.request.id);
if (!user) {
return callback({
code: grpc.status.NOT_FOUND,
message: `User ${call.request.id} not found`,
});
}
callback(null, user);
},
// Server streaming RPC — stream all users
listUsers(call: grpc.ServerWritableStream<any, any>) {
const { page = 1, page_size = 10 } = call.request;
const allUsers = Array.from(users.values());
const start = (page - 1) * page_size;
const pageUsers = allUsers.slice(start, start + page_size);
// Stream each user individually
for (const user of pageUsers) {
call.write(user);
}
call.end(); // Signal stream completion
},
// Client streaming RPC — receive stream of users to create
batchCreateUsers(
call: grpc.ServerReadableStream<any, any>,
callback: grpc.sendUnaryData<any>
) {
const created: any[] = [];
call.on('data', (request) => {
const user = {
id: nextId++,
name: request.name,
email: request.email,
role: request.role || 'VIEWER',
created_at: Date.now(),
};
users.set(user.id, user);
created.push(user);
});
call.on('end', () => {
callback(null, {
users: created,
total_count: created.length,
has_next_page: false,
});
});
},
// Bidirectional streaming — chat
chat(call: grpc.ServerDuplexStream<any, any>) {
call.on('data', (message) => {
console.log(`[${message.user_id}]: ${message.text}`);
// Echo back with server timestamp
call.write({
...message,
timestamp: Date.now(),
});
});
call.on('end', () => call.end());
},
};
// Start server
const server = new grpc.Server();
server.addService(proto.user.UserService.service, userService);
server.bindAsync(
'0.0.0.0:50051',
grpc.ServerCredentials.createInsecure(), // Use createSsl() for production
(err, port) => {
if (err) throw err;
console.log(`gRPC server running on port ${port}`);
}
);
Client Implementation
// src/client.ts
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import path from 'path';
const packageDef = protoLoader.loadSync(
path.join(__dirname, '../proto/user.proto'),
{ keepCase: true, longs: String, enums: String, defaults: true, oneofs: true }
);
const proto = grpc.loadPackageDefinition(packageDef) as any;
// Create client
const client = new proto.user.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);
// Promisify unary calls
function createUser(name: string, email: string): Promise<any> {
return new Promise((resolve, reject) => {
client.createUser({ name, email }, (err: any, user: any) => {
if (err) reject(err);
else resolve(user);
});
});
}
function getUser(id: number): Promise<any> {
return new Promise((resolve, reject) => {
client.getUser({ id }, (err: any, user: any) => {
if (err) reject(err);
else resolve(user);
});
});
}
// Server streaming client
async function listAllUsers(): Promise<any[]> {
return new Promise((resolve, reject) => {
const call = client.listUsers({ page: 1, page_size: 100 });
const users: any[] = [];
call.on('data', (user: any) => users.push(user));
call.on('end', () => resolve(users));
call.on('error', reject);
});
}
// Client streaming
async function batchCreate(userList: { name: string; email: string }[]): Promise<any> {
return new Promise((resolve, reject) => {
const call = client.batchCreateUsers((err: any, response: any) => {
if (err) reject(err);
else resolve(response);
});
// Send each user as a stream message
for (const user of userList) {
call.write(user);
}
call.end();
});
}
// Bidirectional streaming — chat
function startChat(userId: string) {
const call = client.chat();
call.on('data', (message: any) => {
console.log(`Server echo: [${message.user_id}] ${message.text}`);
});
call.on('end', () => console.log('Chat ended'));
// Send messages
call.write({ user_id: userId, text: 'Hello!' });
call.write({ user_id: userId, text: 'How are you?' });
call.end();
}
// Usage
async function main() {
const user = await createUser('Alice', '[email protected]');
console.log('Created:', user);
const fetched = await getUser(user.id);
console.log('Fetched:', fetched);
const allUsers = await listAllUsers();
console.log('All users:', allUsers.length);
}
main().catch(console.error);
Error Handling
// gRPC status codes
import { status } from '@grpc/grpc-js';
// Server: return gRPC errors
callback({
code: status.NOT_FOUND, // 5
message: 'User not found',
});
callback({
code: status.INVALID_ARGUMENT, // 3
message: 'Email is required',
});
callback({
code: status.ALREADY_EXISTS, // 6
message: 'Email already registered',
});
callback({
code: status.PERMISSION_DENIED, // 7
message: 'Admin access required',
});
callback({
code: status.INTERNAL, // 13
message: 'Database error',
});
// Client: handle gRPC errors
try {
const user = await getUser(999);
} catch (err: any) {
if (err.code === status.NOT_FOUND) {
console.log('User not found');
} else if (err.code === status.UNAVAILABLE) {
console.log('Service unavailable — retry');
}
}
Metadata (Headers)
// Client: send metadata (like HTTP headers)
const metadata = new grpc.Metadata();
metadata.add('authorization', 'Bearer my-jwt-token');
metadata.add('x-request-id', 'abc123');
client.getUser({ id: 1 }, metadata, (err, user) => { /* ... */ });
// Server: read metadata
function getUser(call: grpc.ServerUnaryCall<any, any>, callback: any) {
const token = call.metadata.get('authorization')[0];
if (!token) {
return callback({ code: grpc.status.UNAUTHENTICATED });
}
// Verify token...
}
Interceptors (Middleware)
// Client-side interceptor — add auth token to all calls
const authInterceptor: grpc.Interceptor = (options, nextCall) => {
return new grpc.InterceptingCall(nextCall(options), {
start(metadata, listener, next) {
metadata.add('authorization', `Bearer ${getToken()}`);
next(metadata, listener);
},
});
};
const client = new proto.user.UserService(
'localhost:50051',
grpc.credentials.createInsecure(),
{ interceptors: [authInterceptor] }
);
TLS for Production
// Server with TLS
import fs from 'fs';
const credentials = grpc.ServerCredentials.createSsl(
fs.readFileSync('ca.crt'),
[{
cert_chain: fs.readFileSync('server.crt'),
private_key: fs.readFileSync('server.key'),
}],
true // Require client cert (mutual TLS)
);
server.bindAsync('0.0.0.0:50051', credentials, callback);
// Client with TLS
const clientCredentials = grpc.credentials.createSsl(
fs.readFileSync('ca.crt'),
fs.readFileSync('client.key'),
fs.readFileSync('client.crt'),
);
const client = new proto.user.UserService('api.example.com:50051', clientCredentials);
gRPC vs REST vs GraphQL
| Aspect | REST | GraphQL | gRPC |
|---|---|---|---|
| Protocol | HTTP/1.1 | HTTP/1.1 | HTTP/2 |
| Format | JSON | JSON | Protobuf (binary) |
| Performance | Baseline | ~Same as REST | 5-10x faster |
| Browser support | ✅ Native | ✅ Native | ⚠️ Needs grpc-web |
| Schema | Optional (OpenAPI) | Required | Required (.proto) |
| Streaming | ❌ (SSE workaround) | ✅ Subscriptions | ✅ Native, 4 patterns |
| Best for | Public APIs | Flexible client needs | Internal microservices |
Related posts: