Rishabh Mishra

NodeJS Microservice with gRPC and TypeScript

18 April 2023

It is very difficult for an engineering team to manage a very large application. Thus instead of building a large monolith application, companies are writing multiple microservices which can be managed by multiple small teams. These microservices are loosely coupled to each other and can be developed, deployed, and scaled independently. However, in this microservice architecture, the microservices often need to communicate with each other via a lightweight communication protocol. For this inter-process communication, gRPC is the popular choice of developers.

gRPC is an open-source framework for Remote Procedure Calls. It is developed by Google. It is a lightweight, high-performant, and type-safe framework for inter-service communication. It uses http/2 for transport and protocol buffers for serializing structured data.

In this blog, we will write microservices with nodejs and use gRPC for communication between them.

Repo Setup

We will create a monorepo where we will keep all our microservices and protobuf files. We will have a server called product-service and one client to test the server.

Let's create our project repo and change the current working directory to it.

mkdir nodejs-microservices
cd nodejs-microservices

We will create an empty node project inside it. Here we will pass the -y flag to accept all the defaults. We will use npm workspace for local shared dependencies resolution.

npm init -y

We will create 2 directories packages, and services. In the packages directory, we will keep all our reusable libraries like generated Typescript files from protobuf etc. And in the services directory, we will keep all our microservices.

mkdir packages services

Let's create the protos package first.

NOTE: This is not an ideal way to use protobuf files, as the version is not locked in the services. So any changes we make in the protos package will be auto-reflected in services. It is better to keep protobuf files in a separate repo and publish generated Typescript files to a registry. And use that in services as an npm package with a fixed version. You can read about publishing this package to npm in my other blog post

npm init -w packages/protos --scope=@nodejs-microservices -y

In the protos package, we will create 2 directories src and dist. In the src directory, we will keep our protobuf files and in the dist directory, we will keep the generated Typescript files from the protobuf files.

cd packages/protos
mkdir src dist

After this, the project structure should look something like this.

.
├── package.json
├── packages
│   └── protos
│       ├── dist
│       ├── package.json
│       └── src
└── services

Now we will create protobuf files in the src directory. This protos package will contain multiple protobuf files. So, we will create subdirectories inside the src directory as per the proto package name.

Create Protobuf file

For the product-service, we will create product.proto file inside the product subdirectory. We will define the Product message here. Read more about the protobuf message here

src/product/product.proto

syntax = "proto3";

package product;

import "google/protobuf/timestamp.proto";

message Product {
  int32 id = 1;
  string name = 2;
  string description = 3;
  string image = 4;
  repeated string tags = 5;
  google.protobuf.Timestamp created_at = 6;
  google.protobuf.Timestamp updated_at = 7;
}

We will create a service called ProductService and define the schemas of Request and Response for the rpc methods. The ProductService will have 3 rpc methods CreateProduct, GetProduct, and ListProducts.

syntax = "proto3";

package product;

import "google/protobuf/timestamp.proto";

message Product {
  int32 id = 1;
  string name = 2;
  string description = 3;
  string image = 4;
  repeated string tags = 5;
  google.protobuf.Timestamp created_at = 6;
  google.protobuf.Timestamp updated_at = 7;
}

message CreateProductRequest {
  string name = 1;
  string description = 2;
  string image = 3;
  repeated string tags = 4;
}

message CreateProductResponse {
  Product product = 1;
}

message GetProductRequest {
  int32 id = 1;
}

message GetProductResponse {
  Product product = 1;
}

message ListProductsRequest {

}

message ListProductsResponse {
  repeated Product products = 1;
}

service ProductService {
  rpc CreateProduct(CreateProductRequest) returns (CreateProductResponse);
  rpc GetProduct(GetProductRequest) returns (GetProductResponse);
  rpc ListProducts(ListProductsRequest) returns (ListProductsResponse);
}

Generating Typescript files from Protobuf

To Generate the TypeScript files, we need to first install the protobuf compiler. The protobuf compiler will be installed globally in the system. Here I am installing in macos via brew. It will different for other operating systems. check the github repo for installation instructions.

brew install protobuf

We also need to install the ts-proto npm package to use it as a plugin for protoc. We will create a build script to generate typescript files from protbuf. In the build script, we will set dist as the output directory and src as the source directory for protobuf files.

npm i -D ts-proto typescript
touch build.sh

build.sh

#!/bin/bash
protoc --plugin=$(npm root)/.bin/protoc-gen-ts_proto \
 --ts_proto_out=dist \
 --ts_proto_opt=outputServices=grpc-js \
 --ts_proto_opt=esModuleInterop=true \
 -I=src/ src/**/*.proto

In the build script, we are using $(npm root) instead of ./node_modules because due to the npm workspace, the binary is installed in the root node_modules. Read more about npm root here.

We will make the build script executable and run it.

chmod +x build.sh
./build.sh

After executing the build script, the typescript files will be generated in the dist folder, and folder structures will look like this.

.
├── dist
│   ├── google
│   │   └── protobuf
│   │       └── timestamp.ts
│   └── product
│       └── product.ts
├── package.json
└── src
    └── product
        └── product.proto

Create gRPC Server

Let's create the product-service. The product-service will be a gRPC server that will store the product data in the postgres database. We will use typeorm in this service.

From the root of the repo, we will run the npm init command to scaffold the package.json file.

npm init -w services/product-service --scope=@nodejs-microservices -y

Then change the current working directory to product-service. In the product-service directory, we will create src, and dist subdirectories similar to the protos package.

cd services/product-service
mkdir src dist

Let's install the dependencies and dev dependencies for the server.

npm i -D typescript ts-node
npm i @grpc/grpc-js typeorm reflect-metadata pg

We will create the tsconfig.json file inside the server directory next to the package.json file.

tsconfig.json

{
  "compilerOptions": {
    "target": "es2016",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "module": "commonjs",
    "rootDir": "./src",
    "outDir": "./dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

Let's create a Model for the products table. The id is the primary key of the table and will be auto-generated with an auto-increment value. The createdAt & updatedAt fields will be auto-generated too and set during insert and update operations.

src/models/product.ts

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
} from "typeorm";

@Entity()
export class Product {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  name!: string;

  @Column()
  description!: string;

  @Column()
  image!: string;

  @Column("text", { array: true })
  tags!: string[];

  @CreateDateColumn()
  createdAt!: Date;

  @UpdateDateColumn()
  updatedAt!: Date;
}

Let's configure the database connection. We will create db/index.ts file inside the src directory. We have to import the Product model here and add it to the entities property.

src/db/index.ts

import { DataSource } from "typeorm";
import { Product } from "../models/product";

const dataSource = new DataSource({
  type: "postgres",
  host: process.env.POSTGRES_HOST || "localhost",
  port: Number(process.env.POSTGRES_PORT) || 5432,
  username: process.env.POSTGRES_USER || "postgres",
  password: process.env.POSTGRES_PASSWORD || "postgres",
  database: process.env.POSTGRES_DB || "postgres",
  entities: [Product],
  logging: true,
  synchronize: true,
});

export default dataSource;

Let's setup the gRPC server. We will create a main.ts file inside the src directory. Here we will import the dataSource. After initializing the dataSource, we will get the db instance. We will start the server only after the database is connected.

src/main.ts

import "reflect-metadata";
import dataSource from "./db";
import { Server, ServerCredentials } from "@grpc/grpc-js";

const server = new Server();

const HOST = process.env.HOST || "0.0.0.0";
const PORT = Number(process.env.PORT) || 50051;

const address = `${HOST}:${PORT}`;

dataSource
  .initialize()
  .then((db) => {
    server.bindAsync(
      address,
      ServerCredentials.createInsecure(),
      (error, port) => {
        if (error) {
          throw error;
        }
        console.log("server is running on", port);
        server.start();
      }
    );
  })
  .catch((error) => console.log(error));

Update the scripts in the package.json file to run and build the server.

package.json

{
  ...
  "scripts": {
    "dev": "ts-node src/main.ts",
    "build": "tsc",
    "start": "node dist/main.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  ...
}

We can test the server and database connection by running it. It should create the products table in the database.

npm run dev

or

npm run build
npm start

Let's start implementing the methods for the server. First, we need to install the protos package. Then we create a server.ts file.

npm i @nodejs-microservices/protos
touch src/server.ts

We will create a getProductServer function in the server.ts file which will return an object with createProduct, getProduct, and listProducts methods. We are using this server object instead of creating a class due to the server signature issue

We will define the business logic later, right now all the methods will return the status UNIMPLEMENTED in the response. We are doing this to test the server.

src/server.ts

import { sendUnaryData, ServerUnaryCall, status } from "@grpc/grpc-js";
import {
  CreateProductRequest,
  CreateProductResponse,
  GetProductRequest,
  GetProductResponse,
  ListProductsRequest,
  ListProductsResponse,
  ProductServiceServer,
} from "@nodejs-microservices/protos/dist/product/product";
import { DataSource } from "typeorm";

export function getProductServer(db: DataSource): ProductServiceServer {
  async function createProduct(
    call: ServerUnaryCall<CreateProductRequest, CreateProductResponse>,
    callback: sendUnaryData<CreateProductResponse>
  ) {
    callback({ code: status.UNIMPLEMENTED }, null);
  }
  async function getProduct(
    call: ServerUnaryCall<GetProductRequest, GetProductResponse>,
    callback: sendUnaryData<GetProductResponse>
  ) {
    callback({ code: status.UNIMPLEMENTED }, null);
  }
  async function listProducts(
    call: ServerUnaryCall<ListProductsRequest, ListProductsResponse>,
    callback: sendUnaryData<ListProductsResponse>
  ) {
    callback({ code: status.UNIMPLEMENTED }, null);
  }

  return {
    createProduct,
    getProduct,
    listProducts,
  };
}

We will import the getProductServer function and ProductService definition in the main.ts file and then add the product service to the gRPC server.

src/main.ts

import "reflect-metadata";
import dataSource from "./db";
import { Server, ServerCredentials } from "@grpc/grpc-js";
import { getProductServer } from "./server";
import { ProductServiceService } from "@nodejs-microservices/protos/dist/product/product";

const server = new Server();

const HOST = process.env.HOST || "0.0.0.0";
const PORT = Number(process.env.PORT) || 50051;

const address = `${HOST}:${PORT}`;

dataSource
  .initialize()
  .then((db) => {
    server.addService(ProductServiceService, getProductServer(db));
    server.bindAsync(
      address,
      ServerCredentials.createInsecure(),
      (error, port) => {
        if (error) {
          throw error;
        }
        console.log("server is running on", port);
        server.start();
      }
    );
  })
  .catch((error) => console.log(error));

After running the server, We will test the service methods with grpcurl. Read more about grpcurl here

Let's test the CreateProduct method, we need to pass the proto file to the grpcurl command. I am calling the grpcurl from the root of the monorepo, thus the path to the proto files are relatives to the root. After that, We need to pass the request body as a JSON string. Then pass the address to the server and service method name.

grpcurl -import-path packages/protos/src -proto product/product.proto -d '{ "name": "product 1", "description": "foo bar", "image": "", "tags": ["tag 1"]}' -plaintext localhost:50051 product.ProductService.CreateProduct

ERROR:
  Code: Unimplemented
  Message: Unknown Error

After running the grpcurl call, we should get an error from the server. Here we are getting the status code Unimplemented as we expected.

Let's add the controller for product model. We will create product.controller.ts file inside src/controllers directory. In this file we will write the logic for database operations. We create the createProduct function, the function accepts the database instance and request body as the arguments.

We are defining the interface for createProductReq here, We can also use CreateProductRequest from the protos package instead of this. It is just my personal preference to define the interface inside the server as we are mapping the request fields with Product model fields. So, If in the future there is a change in CreateProductRequest then we can easily transform CreateProductRequest to createProductReq

src/controllers/product.controller.ts

import { DataSource } from "typeorm";
import { Product } from "../models/product";

interface createProductReq {
  name: string;
  description: string;
  image: string;
  tags: string[];
}

export const createProduct = async (
  db: DataSource,
  req: createProductReq
): Promise<Product> => {
  const productRepository = db.getRepository(Product);
  const product = new Product();
  product.name = req.name;
  product.description = req.description;
  product.image = req.image;
  product.tags = req.tags;
  return productRepository.save(product);
};

We then import the ProductController in the server.ts file and update the createProduct method. We will transform the result from the database insert operation to the Product message by passing it to the fromJSON method. We will wrap this logic inside a try-catch block and return the status code INTERNAL in case of an error.

src/server.ts

import { sendUnaryData, ServerUnaryCall, status } from "@grpc/grpc-js";
import {
  CreateProductRequest,
  CreateProductResponse,
  GetProductRequest,
  GetProductResponse,
  ListProductsRequest,
  ListProductsResponse,
  Product,
  ProductServiceServer,
} from "@nodejs-microservices/protos/dist/product/product";
import { DataSource } from "typeorm";
import * as ProductController from "./controllers/product.controller";

export function getProductServer(db: DataSource): ProductServiceServer {
  async function createProduct(
    call: ServerUnaryCall<CreateProductRequest, CreateProductResponse>,
    callback: sendUnaryData<CreateProductResponse>
  ) {
    try {
      const product = await ProductController.createProduct(db, call.request);
      const productPB = Product.fromJSON(product);
      const response: CreateProductResponse = {
        product: productPB,
      };
      callback(null, response);
    } catch (err) {
      callback({ code: status.INTERNAL }, null);
      console.error(err);
    }
  }
  async function getProduct(
    call: ServerUnaryCall<GetProductRequest, GetProductResponse>,
    callback: sendUnaryData<GetProductResponse>
  ) {
    callback({ code: status.UNIMPLEMENTED }, null);
  }
  async function listProducts(
    call: ServerUnaryCall<ListProductsRequest, ListProductsResponse>,
    callback: sendUnaryData<ListProductsResponse>
  ) {
    callback({ code: status.UNIMPLEMENTED }, null);
  }

  return {
    createProduct,
    getProduct,
    listProducts,
  };
}

Let's test the CreateProduct method again with grpcurl. We should get the response back from the server now instead of the status UNIMPLEMENTED error.

grpcurl -import-path packages/protos/src -proto product/product.proto -d '{ "name": "product 1", "description": "foo bar", "image": "", "tags": ["tag 1"]}' -plaintext localhost:50051 product.ProductService.CreateProduct

{
  "product": {
    "id": 1,
    "name": "product 1",
    "description": "foo bar",
    "tags": [
      "tag 1"
    ],
    "createdAt": "2023-03-29T00:07:23.426Z",
    "updatedAt": "2023-03-29T00:07:23.426Z"
  }
}

Let's add the listProducts and getProduct functions to the product controller. Both functions will accept database instance as an argument similar to the createProduct function

src/controllers/product.controller.ts

import { DataSource } from "typeorm";
import { Product } from "../models/product";

interface createProductReq {
  name: string;
  description: string;
  image: string;
  tags: string[];
}

export const createProduct = async (
  db: DataSource,
  req: createProductReq
): Promise<Product> => {
  const productRepository = db.getRepository(Product);
  const product = new Product();
  product.name = req.name;
  product.description = req.description;
  product.image = req.image;
  product.tags = req.tags;
  return productRepository.save(product);
};

export const listProducts = async (db: DataSource): Promise<Product[]> => {
  const productRepository = db.getRepository(Product);
  return productRepository.find();
};

export const getProduct = async (
  db: DataSource,
  id: number
): Promise<Product | null> => {
  const productRepository = db.getRepository(Product);
  return productRepository.findOneBy({ id: id });
};

We will update the getProduct and listProducts methods in the server.ts file.

In the getProduct method, we will check if the database returns the product, then only we return the response. Otherwise, we will return the status code NOT_FOUND with the message.

In the listProducts method, we will get an array of product objects from the database, we will run the map function to transform each of them into the Product message.

src/server.ts

import { sendUnaryData, ServerUnaryCall, status } from "@grpc/grpc-js";
import {
  CreateProductRequest,
  CreateProductResponse,
  GetProductRequest,
  GetProductResponse,
  ListProductsRequest,
  ListProductsResponse,
  Product,
  ProductServiceServer,
} from "@nodejs-microservices/protos/dist/product/product";
import { DataSource } from "typeorm";
import * as ProductController from "./controllers/product.controller";

export function getProductServer(db: DataSource): ProductServiceServer {
  async function createProduct(
    call: ServerUnaryCall<CreateProductRequest, CreateProductResponse>,
    callback: sendUnaryData<CreateProductResponse>
  ) {
    try {
      const product = await ProductController.createProduct(db, call.request);
      const productPB = Product.fromJSON(product);
      const response: CreateProductResponse = {
        product: productPB,
      };
      callback(null, response);
    } catch (err) {
      callback({ code: status.INTERNAL }, null);
      console.error(err);
    }
  }
  async function getProduct(
    call: ServerUnaryCall<GetProductRequest, GetProductResponse>,
    callback: sendUnaryData<GetProductResponse>
  ) {
    try {
      const product = await ProductController.getProduct(db, call.request.id);
      if (product) {
        const productPB = Product.fromJSON(product);
        const response: GetProductResponse = {
          product: productPB,
        };
        callback(null, response);
      } else {
        callback(
          {
            code: status.NOT_FOUND,
            message: `Product ${call.request.id} not found`,
          },
          null
        );
      }
    } catch (err) {
      callback({ code: status.INTERNAL }, null);
      console.error(err);
    }
  }
  async function listProducts(
    call: ServerUnaryCall<ListProductsRequest, ListProductsResponse>,
    callback: sendUnaryData<ListProductsResponse>
  ) {
    try {
      const products = await ProductController.listProducts(db);
      const productsPB = products.map(Product.fromJSON);
      const response: ListProductsResponse = {
        products: productsPB,
      };
      callback(null, response);
    } catch (err) {
      callback({ code: status.INTERNAL }, null);
      console.error(err);
    }
  }

  return {
    createProduct,
    getProduct,
    listProducts,
  };
}

We will test the ListProducts and GetProduct methods with grpcurl. They shouldn't be returning an error with the status UNIMPLEMENTED.

grpcurl -import-path packages/protos/src -proto product/product.proto -plaintext localhost:50051 product.ProductService.ListProducts

{
  "products": [
    {
      "id": 1,
      "name": "product 1",
      "description": "foo bar",
      "tags": [
        "tag 1"
      ],
      "createdAt": "2023-03-29T00:07:23.426Z",
      "updatedAt": "2023-03-29T00:07:23.426Z"
    }
  ]
}

grpcurl -import-path packages/protos/src -proto product/product.proto -d '{"id": 1}' -plaintext localhost:50051 product.ProductService.GetProduct

{
  "product": {
    "id": 1,
    "name": "product 1",
    "description": "foo bar",
    "tags": [
      "tag 1"
    ],
    "createdAt": "2023-03-29T00:07:23.426Z",
    "updatedAt": "2023-03-29T00:07:23.426Z"
  }
}

Create gRPC Client

Let's create a client to test the product-service. We will create a simple program that will connect to the server, make the call and log the response in the console.

From the root of the repo, we will run the npm init command to scaffold the package.json file and change the current working directory to the test-client. Inside the test-client we will create the src and dist directories.

npm init -w services/test-client --scope=@nodejs-microservices -y
cd services/test-client/
mkdir src dist

We will install the dependencies and dev dependencies for the client. We will install the @nodejs-microservices/protos package to get the client stub.

npm i -D typescript ts-node
npm i @grpc/grpc-js @nodejs-microservices/protos

We will create the tsconfig.json file inside test-client, the tsconfig.json is the same as in the product-service

tsconfig.json

{
  "compilerOptions": {
    "target": "es2016",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "module": "commonjs",
    "rootDir": "./src",
    "outDir": "./dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

We will update the scripts in the package.json file to run and build the client.

package.json

{
  ...
  "scripts": {
    "dev": "ts-node src/main.ts",
    "build": "tsc",
    "start": "node dist/main.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  ...
}

Let's create the main.ts file inside the src directory. We will create a main function and test each service method separately.

First, We need to import the credentials object from the grpc-js package and ProductServiceClient from our protos package. Then we will create a gRPC client. While creating the client instance, we need to pass the server url and authentication credentials. The product-service doesn't have any authentication thus we are passing credentials.createInsecure() as authentication credentials.

After creating the client instance, we will call the service method name. Here we are calling the createProduct method on the client. We will create the request object for the call and then call the method with the request object and callback function.

src/main.ts

import { credentials } from "@grpc/grpc-js";
import {
  CreateProductRequest,
  ProductServiceClient,
} from "@nodejs-microservices/protos/dist/product/product";

const PRODUCT_SERVICE_URL = process.env.USER_SERVICE_URL || "0.0.0.0:50051";

function main() {
  const client = new ProductServiceClient(
    PRODUCT_SERVICE_URL,
    credentials.createInsecure()
  );

  const req: CreateProductRequest = {
    name: "test product",
    description: "foo bar",
    image: "https://example.com/image",
    tags: ["tag-1"],
  };

  client.createProduct(req, (err, resp) => {
    if (err) {
      console.error(err);
    } else {
      console.log("Response :", resp);
    }
  });
}

main();

We can similarly call and test the getProduct and listProducts methods. I am skipping that part in this blog.

All the source code for this blog is available on GitHub.