Building gRPC Services with Node.js and TypeScript
Introduction
gRPC provides efficient, strongly-typed communication between microservices. This guide covers implementing gRPC servers and clients in Node.js with TypeScript.
Prerequisites
- Node.js >=14
- TypeScript
- Protocol Buffers compiler
Step 1: Install Dependencies
npm install @grpc/grpc-js @grpc/proto-loader
npm install -D @types/google-protobuf grpc-tools
Step 2: Define Protocol Buffer Schema
Create protos/user.proto
:
syntax = "proto3";
package user;
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc CreateUser (CreateUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
rpc UpdateUserStream (stream UpdateUserRequest) returns (stream User);
}
message User {
string id = 1;
string name = 2;
string email = 3;
int64 created_at = 4;
}
message GetUserRequest {
string id = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
message ListUsersRequest {
int32 limit = 1;
int32 offset = 2;
}
message UpdateUserRequest {
string id = 1;
User user = 2;
}
Step 3: Generate TypeScript Definitions
Add to package.json
:
{
"scripts": {
"proto:gen": "grpc_tools_node_protoc --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --ts_out=grpc_js:./src/generated --grpc_out=grpc_js:./src/generated --js_out=import_style=commonjs:./src/generated -I ./protos ./protos/*.proto"
}
}
Run: npm run proto:gen
Step 4: Implement gRPC Server
Create 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, '../protos/user.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const userProto = (grpc.loadPackageDefinition(packageDefinition) as any).user;
// Mock database
const users = new Map<string, any>();
const userService = {
GetUser: (call: any, callback: any) => {
const userId = call.request.id;
const user = users.get(userId);
if (user) {
callback(null, user);
} else {
callback({
code: grpc.status.NOT_FOUND,
details: 'User not found'
});
}
},
CreateUser: (call: any, callback: any) => {
const { name, email } = call.request;
const user = {
id: Date.now().toString(),
name,
email,
created_at: Date.now()
};
users.set(user.id, user);
callback(null, user);
},
ListUsers: (call: any) => {
const { limit = 10, offset = 0 } = call.request;
const userList = Array.from(users.values()).slice(offset, offset + limit);
userList.forEach(user => {
call.write(user);
});
call.end();
},
UpdateUserStream: (call: any) => {
call.on('data', (request: any) => {
const { id, user: userData } = request;
const existingUser = users.get(id);
if (existingUser) {
const updatedUser = { ...existingUser, ...userData };
users.set(id, updatedUser);
call.write(updatedUser);
} else {
call.destroy(new Error('User not found'));
}
});
call.on('end', () => {
call.end();
});
}
};
function startServer() {
const server = new grpc.Server();
server.addService(userProto.UserService.service, userService);
const port = '0.0.0.0:50051';
server.bindAsync(port, grpc.ServerCredentials.createInsecure(), (error, port) => {
if (error) {
console.error('Failed to start server:', error);
return;
}
console.log(`gRPC server running on port ${port}`);
server.start();
});
}
if (require.main === module) {
startServer();
}
export { startServer };
Step 5: Implement gRPC Client
Create 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, '../protos/user.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const userProto = (grpc.loadPackageDefinition(packageDefinition) as any).user;
class UserClient {
private client: any;
constructor(address: string = 'localhost:50051') {
this.client = new userProto.UserService(
address,
grpc.credentials.createInsecure()
);
}
async getUser(id: string): Promise<any> {
return new Promise((resolve, reject) => {
this.client.GetUser({ id }, (error: any, response: any) => {
if (error) {
reject(error);
} else {
resolve(response);
}
});
});
}
async createUser(name: string, email: string): Promise<any> {
return new Promise((resolve, reject) => {
this.client.CreateUser({ name, email }, (error: any, response: any) => {
if (error) {
reject(error);
} else {
resolve(response);
}
});
});
}
async listUsers(limit: number = 10, offset: number = 0): Promise<any[]> {
return new Promise((resolve, reject) => {
const users: any[] = [];
const call = this.client.ListUsers({ limit, offset });
call.on('data', (user: any) => {
users.push(user);
});
call.on('end', () => {
resolve(users);
});
call.on('error', (error: any) => {
reject(error);
});
});
}
updateUserStream(): any {
return this.client.UpdateUserStream();
}
close() {
this.client.close();
}
}
export { UserClient };
// Example usage
async function main() {
const client = new UserClient();
try {
// Create user
const newUser = await client.createUser('John Doe', 'john@example.com');
console.log('Created user:', newUser);
// Get user
const user = await client.getUser(newUser.id);
console.log('Retrieved user:', user);
// List users
const users = await client.listUsers(5, 0);
console.log('Users list:', users);
} catch (error) {
console.error('Client error:', error);
} finally {
client.close();
}
}
if (require.main === module) {
main();
}
Step 6: Streaming Example
Bidirectional streaming implementation:
import { UserClient } from './client';
async function streamingExample() {
const client = new UserClient();
const stream = client.updateUserStream();
// Handle responses
stream.on('data', (user: any) => {
console.log('Updated user received:', user);
});
stream.on('end', () => {
console.log('Stream ended');
client.close();
});
stream.on('error', (error: any) => {
console.error('Stream error:', error);
client.close();
});
// Send updates
stream.write({
id: '1',
user: { name: 'Updated Name', email: 'updated@example.com' }
});
stream.write({
id: '2',
user: { name: 'Another Update', email: 'another@example.com' }
});
// End the stream
stream.end();
}
Step 7: Health Check Implementation
Add health checking to protos/health.proto
:
syntax = "proto3";
package grpc.health.v1;
service Health {
rpc Check (HealthCheckRequest) returns (HealthCheckResponse);
}
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
}
ServingStatus status = 1;
}
Step 8: Error Handling and Interceptors
import * as grpc from '@grpc/grpc-js';
export function loggingInterceptor(options: any, nextCall: any) {
return new grpc.InterceptingCall(nextCall(options), {
start: function(metadata, listener, next) {
console.log(`gRPC call started: ${options.method_definition.path}`);
next(metadata, {
onReceiveMessage: (message, next) => {
console.log('Received message:', message);
next(message);
},
onReceiveStatus: (status, next) => {
console.log('Call status:', status.code);
next(status);
}
});
}
});
}
Summary
gRPC with Node.js and TypeScript provides high-performance, type-safe communication between microservices. Use Protocol Buffers for schema definition, implement streaming for real-time data, and add interceptors for cross-cutting concerns.