Creating a gRPC server and client with Node.js and TypeScript

Radovan Stevanovic
Nerd For Tech
Published in
6 min readJan 28, 2023

--

gRPC is a high-performance, open-source framework for building remote procedure call (RPC) APIs. It allows you to define a service using a Protocol Buffer IDL file and generate server and client code in multiple languages. This makes it easy to create a service that can be consumed by clients written in different languages, including Node.js.

This tutorial will create a simple gRPC service using Node.js and TypeScript. The service will have four methods:

  • createAddress: Returns a user address (hex) string.
  • transaction: Accepts a toAddress, points, and metadata as input and returns a transaction ID.
  • balance: Accepts an address and returns the total and available balance.
  • walletInfo: Accepts an address and returns the total, available balance, and a list of transactions.

Defining the service

The first step in creating a gRPC service is to define the service using a Protocol Buffer IDL file. Here's an example of what the IDL file might look like for our service:

syntax = "proto3";

service Wallet {
rpc createAddress (CreateAddressRequest) returns (CreateAddressResponse);
rpc transaction (TransactionRequest) returns (TransactionResponse);
rpc balance (BalanceRequest) returns (BalanceResponse);
rpc walletInfo (WalletInfoRequest) returns (WalletInfoResponse);
}

message CreateAddressRequest { }

message CreateAddressResponse {
string address = 1;
}

message TransactionRequest {
string toAddress = 1;
int32 points = 2;
string metadata = 3;
}

message TransactionResponse {
int64 transaction_id = 1;
}

message BalanceRequest {
string address = 1;
}

message BalanceResponse {
int64 total = 1;
int64 available = 2;
}

message WalletInfoRequest {
string address = 1;
}

message WalletInfoResponse {
int64 total = 1;
int64 available = 2;
repeated Transaction transactions = 3;
}

message Transaction {
string to_address = 1;
int32 points = 2;
string metadata = 3;
}p

Generating the code

Once we have our IDL file defined, we can use the protoc compiler to generate the gRPC code for our service in Node.js and TypeScript. We will also need to install the @grpc/proto-loader and grpc npm packages to use with the generated code.

Here's an example of how you might run the protoc compiler:

protoc --js_out=import_style=commonjs,binary:./ --grpc_out=./ --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` wallet.proto

This will generate the following files:

  • wallet_grpc_pb.js and wallet_grpc_pb.d.ts: The gRPC client and server code.
  • wallet_pb.js and wallet_pb.d.ts: The code for the request and response messages.

Implementing the service

Now that we have the generated code, we can use it to implement our service.

async function walletInfo(call: ServerUnaryCall<string>, callback: sendUnaryData<WalletInfoResponse>) {
const address = call.request;
try {
const result = {
total: 100,
available: 80,
};
const transactions = [
{
toAddress: '0x1234567890',
value: 10,
timestamp: Date.now(),
metadata: {},
},
{
toAddress: '0x0987654321',
value: 20,
timestamp: Date.now(),
metadata: {},
},
];
callback(null, {
total: result.total,
available: result.available,
transactions: transactions
});
} catch (err) {
callback(err, null);
}
}
import { BalanceResponse, BalanceRequest } from './wallet_pb';

function balance(call: ServerUnaryCall<BalanceRequest>, callback: sendUnaryData<BalanceResponse>) {
// Perform necessary business logic
const total = 100;
const available = 50;
const balanceResponse = new BalanceResponse();
balanceResponse.setTotal(total);
balanceResponse.setAvailable(available);
callback(null, balanceResponse);
}
import { TransactionResponse, TransactionRequest } from './wallet_pb';

function transaction(call: ServerUnaryCall<TransactionRequest>, callback: sendUnaryData<TransactionResponse>) {
// Perform necessary business logic
const transactionId = Date.now();
const transactionResponse = new TransactionResponse();
transactionResponse.setTransactionId(transactionId);
callback(null, transactionResponse);
}
import { CreateAddressResponse, CreateAddressRequest } from './wallet_pb';

function createAddress(call: ServerUnaryCall<CreateAddressRequest>, callback: sendUnaryData<CreateAddressResponse>) {
// Perform necessary business logic
const address = '0x1234567890abcdef';
const createAddressResponse = new CreateAddressResponse();
createAddressResponse.setAddress(address);
callback(null, createAddressResponse);
}

And finally, here's an example of how you might set up and start the gRPC server:

import * as grpc from 'grpc';
import { WalletService } from './wallet_grpc_pb';

const server = new grpc.Server();
server.addService(WalletService.service, {
createAddress,
transaction,
balance,
walletInfo
});
server.bind('0.0.0.0:50051', grpc.ServerCredentials.createInsecure());
server.start();

This will start a gRPC server on the local machine and port 50051. The server will have the four service handlers we defined earlier and use an insecure (plaintext) connection.

Creating the client

Now that we have our gRPC server, we can create a client to connect to it.

Here's an example of how you might create a client and call the walletInfo service:

import { WalletServiceClient } from './wallet_grpc_pb';
import { WalletInfoRequest, WalletInfoResponse } from './wallet_pb';

const client = new WalletServiceClient('localhost:50051', grpc.credentials.createInsecure());
const request = new WalletInfoRequest();
request.setAddress('0x1234567890abcdef');
client.walletInfo(request, (error, response) => {
if (error) {
console.error(error);
} else {
console.log(response.getTotal());
console.log(response.getAvailable());
console.log(response.getTransactionsList());
}
});

This will create a gRPC client that connects to the server we set up earlier and calls the walletInfo service with the provided address. The client will be using an insecure (plaintext) connection.

Persistence

To store the transactions, you need a database. A common choice for an append-only transaction store is a distributed ledger technology (DLT) database like Blockchain. However, we will use a relational database like Postgres to keep it simple for this example.

You must install Postgres and create a new database and table to store the transactions. The table should have the following columns:

  • from: a string representing the sender's address
  • to: a string representing the receiver's address
  • value: an integer representing the number of points transferred
  • timestamp: a timestamp representing the time of the transaction
  • metadata: a JSON object representing additional information about the transaction

You can use an ORM (Object-Relational Mapper) like TypeORM to interact with the database in your Node.js application. Typeform is a popular ORM for TypeScript. It provides a way to interact with a database using an object-oriented approach.

Here is an example of how you might use TypeORM to create a transaction and store it in the database:

import { Entity, getConnection, getManager } from 'typeorm';

@Entity()
class Transaction {
@PrimaryGeneratedColumn()
id: number;

@Column()
from: string;

@Column()
to: string;

@Column()
value: number;

@Column("timestamp")
timestamp: Date;

@Column("json")
metadata: any;
}

async function createTransaction(from: string, to: string, value: number, metadata: any) {
const transaction = new Transaction();
transaction.from = from;
transaction.to = to;
transaction.value = value;
transaction.timestamp = new Date();
transaction.metadata = metadata;

await getManager().save(transaction);
}

Updated walletInfo method:

async function walletInfo(call: ServerUnaryCall<string>, callback: sendUnaryData<WalletInfoResponse>) {
const address = call.request;
try {
const result = await db.query(`SELECT SUM(value) as total, SUM(CASE WHEN to_address = '${address}' THEN value ELSE 0 END) as available FROM transactions WHERE from_address = '${address}'`);
const transactions = await db.query(`SELECT to_address, value, timestamp, metadata FROM transactions WHERE from_address = '${address}'`);
callback(null, {
total: result[0].total,
available: result[0].available,
transactions: transactions
});
} catch (err) {
callback(err, null);
}
}

It's worth noting that the above code uses an ORM library to interact with the database, and the query is written in plain SQL. Depending on your ORM library, you may have to write the query differently.

Also, please make sure to use Prepared statements to avoid SQL injection and use transactions if you are doing multiple operations on the database.

Final Words

This tutorial shows how to create a gRPC server and client using Node.js and TypeScript. We have defined a simple service with four methods: createAddress, transaction, balance, and walletInfo. We have also implemented a mock for the service handlers and a simple MySQL database for storing the transactions.

While this example is relatively simple, it demonstrates the basics of building and integrating a gRPC service with a database. You need to consider security, scalability, and other factors in a production system, but this example should serve as a good starting point.

Building gRPC services can be a great way to build high-performance, low-latency systems that can handle many concurrent requests. Protocol Buffers for data serialization make the service highly efficient and easy to evolve.

In addition, gRPC has a lot of great features, such as flow control, bi-directional streaming, and flow control. It also has official support for many languages and platforms, making it easy to build a polyglot system.

If you're interested in building gRPC services, we recommend you check the official documentation and explore the many online resources. We hope this tutorial has been helpful and that you will enjoy building your own gRPC services.

--

--

Radovan Stevanovic
Nerd For Tech

Curiosity in functional programming, cybersecurity, and blockchain drives me to create innovative solutions and continually improve my skills