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
.protofiles 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
- Documentation: HazelJS gRPC Package Docs
- npm: @hazeljs/grpc
- HazelJS Blog: High-Performance Microservices with @hazeljs/grpc