Skip to content
Go back

Building gRPC Services with Node.js and TypeScript

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

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.


Share this post on:

Previous Post
Scaling WebSocket Connections with Redis and Clustering
Next Post
Implementing Redis Caching Strategies in Node.js