I recently added gRPC support to HazelJS — a TypeScript-first Node.js framework. The new @hazeljs/grpc package lets you build gRPC servers with a decorator-based API, full dependency injection, and the familiar module pattern.

Why gRPC?

Microservices need efficient, type-safe communication. gRPC gives you high-performance RPC over HTTP/2 with Protocol Buffers for serialization. Instead of wiring proto loading, service registration, and handlers manually, the HazelJS gRPC module handles it with decorators and DI.

Features

  • Decorator-based handlers — Use @GrpcMethod('ServiceName', 'MethodName') to declare RPC handlers
  • Full DI integration — Controllers are resolved from the HazelJS container; inject services and repositories
  • Proto loading — Load .proto files at runtime with configurable options
  • Discovery compatible — Works with the HazelJS Discovery package for service registration (protocol: 'grpc')

Installation

npm install @hazeljs/grpc

npm: @hazeljs/grpc

Quick Start

Define your service in a .proto file:

syntax = "proto3";
package hero;

service HeroService {
  rpc FindOne (HeroById) returns (Hero);
}

message HeroById { int32 id = 1; }
message Hero { int32 id = 1; string name = 2; }

Create a controller with @GrpcMethod:

import { Injectable, HazelModule } from '@hazeljs/core';
import { GrpcMethod, GrpcModule } from '@hazeljs/grpc';
import { join } from 'path';

@Injectable()
export class HeroGrpcController {
  @GrpcMethod('HeroService', 'FindOne')
  findOne(data: { id: number }) {
    return { id: data.id, name: 'Hero' };
  }
}

@HazelModule({
  imports: [
    GrpcModule.forRoot({
      protoPath: join(__dirname, 'hero.proto'),
      package: 'hero',
      url: '0.0.0.0:50051',
    }),
  ],
  providers: [HeroGrpcController],
})
export class AppModule {}

Register handlers and start the gRPC server after your HTTP server:

import { HazelApp } from '@hazeljs/core';
import { GrpcModule, GrpcServer } from '@hazeljs/grpc';
import { Container } from '@hazeljs/core';

const app = new HazelApp(AppModule);
GrpcModule.registerHandlersFromProviders([HeroGrpcController]);
await app.listen(3000);

const grpcServer = Container.getInstance().resolve(GrpcServer);
await grpcServer.start();

Concrete Example: Product Catalog Service

Here's a complete example — a product catalog gRPC service that fetches products from a repository and supports both lookup by ID and listing with pagination.

product.proto

syntax = "proto3";
package catalog;

service ProductService {
  rpc GetProduct (GetProductRequest) returns (Product);
  rpc ListProducts (ListProductsRequest) returns (ListProductsResponse);
}

message GetProductRequest {
  string id = 1;
}

message Product {
  string id = 1;
  string name = 2;
  string description = 3;
  double price = 4;
}

message ListProductsRequest {
  int32 page = 1;
  int32 pageSize = 2;
}

message ListProductsResponse {
  repeated Product products = 1;
  int32 total = 2;
}

product.repository.ts — In-memory store (replace with Prisma/DB in production):

import { Injectable } from '@hazeljs/core';

@Injectable()
export class ProductRepository {
  private products = [
    { id: '1', name: 'Widget A', description: 'A useful widget', price: 9.99 },
    { id: '2', name: 'Widget B', description: 'Another widget', price: 14.99 },
    { id: '3', name: 'Gadget X', description: 'A fancy gadget', price: 29.99 },
  ];

  findById(id: string) {
    return this.products.find((p) => p.id === id) ?? null;
  }

  findAll(page: number, pageSize: number) {
    const start = (page - 1) * pageSize;
    const items = this.products.slice(start, start + pageSize);
    return { products: items, total: this.products.length };
  }
}

product.grpc-controller.ts — gRPC controller with injected repository:

import { Injectable } from '@hazeljs/core';
import { GrpcMethod } from '@hazeljs/grpc';
import { ProductRepository } from './product.repository';

@Injectable()
export class ProductGrpcController {
  constructor(private readonly productRepo: ProductRepository) {}

  @GrpcMethod('ProductService', 'GetProduct')
  async getProduct(data: { id: string }) {
    const product = this.productRepo.findById(data.id);
    if (!product) {
      throw new Error(`Product ${data.id} not found`);
    }
    return product;
  }

  @GrpcMethod('ProductService', 'ListProducts')
  async listProducts(data: { page?: number; pageSize?: number }) {
    const page = data.page ?? 1;
    const pageSize = data.pageSize ?? 10;
    return this.productRepo.findAll(page, pageSize);
  }
}

app.module.ts

import { HazelModule } from '@hazeljs/core';
import { GrpcModule } from '@hazeljs/grpc';
import { join } from 'path';
import { ProductGrpcController } from './product.grpc-controller';
import { ProductRepository } from './product.repository';

@HazelModule({
  imports: [
    GrpcModule.forRoot({
      protoPath: join(__dirname, 'product.proto'),
      package: 'catalog',
      url: '0.0.0.0:50051',
    }),
  ],
  providers: [ProductRepository, ProductGrpcController],
})
export class AppModule {}

main.ts

import { HazelApp } from '@hazeljs/core';
import { GrpcModule, GrpcServer } from '@hazeljs/grpc';
import { Container } from '@hazeljs/core';
import { AppModule } from './app.module';
import { ProductGrpcController } from './product.grpc-controller';

async function bootstrap() {
  const app = new HazelApp(AppModule);
  GrpcModule.registerHandlersFromProviders([ProductGrpcController]);
  await app.listen(3000);

  const grpcServer = Container.getInstance().resolve(GrpcServer);
  await grpcServer.start();

  console.log('HTTP on :3000, gRPC on :50051');
}
bootstrap();

Test with grpcurl:

# Get a product by ID
grpcurl -plaintext -d '{"id":"1"}' localhost:50051 catalog.ProductService/GetProduct

# List products with pagination
grpcurl -plaintext -d '{"page":1,"pageSize":2}' localhost:50051 catalog.ProductService/ListProducts

Learn More