A comprehensive guide to building a gRPC server and gRPC client using HazelJS and @hazeljs/grpc. This starter demonstrates a real-world microservice architecture where an HTTP API (Order service) consumes a gRPC backend (Product service).
Table of Contents
- Why gRPC with HazelJS?
- Architecture Overview
- Prerequisites
- Project Structure
- The Proto Definition
- Product Service (gRPC Server)
- Order Service (gRPC Client)
- Running the Example
- Testing the Flow
- Key Concepts & Patterns
- Further Reading & Resources
Why gRPC with HazelJS?
gRPC is a high-performance RPC framework that uses Protocol Buffers for serialization. It excels in service-to-service communication, offering:
-
Strong typing via
.protoschema definitions - Efficient binary serialization (smaller payloads than JSON)
- Streaming support (unary, server, client, bidirectional)
- Language-agnostic contracts shared across polyglot systems
HazelJS is a TypeScript-first Node.js framework with dependency injection, decorators, and modular architecture. The @hazeljs/grpc package integrates gRPC seamlessly:
-
Decorator-based API –
@GrpcMethod()for declarative RPC handlers - DI integration – Controllers and services resolved from the HazelJS container
-
Unified module system –
GrpcModuleandGrpcClientModuleplug into HazelModule - Built on @grpc/grpc-js – Production-ready Node.js gRPC implementation
This starter shows how to combine both: a gRPC server for internal APIs and an HTTP gateway for external clients.
Architecture Overview
┌─────────────────────────────────────┐ gRPC (localhost:50051) ┌─────────────────────────────────────┐
│ Order Service (HTTP Gateway) │ ──────────────────────────────────────▶│ Product Service (gRPC Backend) │
│ │ │ │
│ • HTTP :3000 │ GetProduct, ListProducts │ • gRPC :50051 │
│ • GrpcClientModule │ │ • GrpcModule │
│ • OrderController → OrderService │ │ • ProductGrpcController │
│ → GrpcClientService │ │ • In-memory product catalog │
└─────────────────────────────────────┘ └─────────────────────────────────────┘
│ │
│ curl http://localhost:3000/products │
│ curl http://localhost:3000/products/1 │
▼ │
External clients Internal service-to-service
Flow:
- External clients call the Order service over HTTP (REST-style).
- OrderController receives requests and delegates to OrderService.
- OrderService injects GrpcClientService and calls the Product gRPC service.
- Product service handles gRPC requests and returns product data.
This pattern is common in microservices: an HTTP API layer for external consumers, with internal services communicating via gRPC for performance and type safety.
Prerequisites
- Node.js >= 18
- npm or pnpm
- Basic familiarity with TypeScript, gRPC, and Protocol Buffers
Project Structure
hazeljs-grpc-starter/
├── proto/
│ └── product.proto # Shared service contract (ProductService)
├── src/
│ ├── product-server/ # gRPC Server
│ │ ├── main.ts # Bootstrap: HazelApp + GrpcModule + GrpcServer
│ │ ├── app.module.ts # ProductAppModule with GrpcModule.forRoot()
│ │ └── product.grpc-controller.ts # @GrpcMethod handlers
│ └── order-client/ # gRPC Client (HTTP API)
│ ├── main.ts # Bootstrap: HazelApp
│ ├── app.module.ts # OrderAppModule with GrpcClientModule.forRoot()
│ ├── order.controller.ts # HTTP routes → OrderService
│ └── order.service.ts # GrpcClientService → Product gRPC
├── package.json
├── tsconfig.json
├── README.md
└── BLOG.md
The Proto Definition
The contract is defined in proto/product.proto:
syntax = "proto3";
package catalog;
service ProductService {
rpc GetProduct (GetProductRequest) returns (Product);
rpc ListProducts (ListProductsRequest) returns (ListProductsResponse);
}
message GetProductRequest {
string id = 1;
}
message ListProductsRequest {
int32 limit = 1;
}
message Product {
string id = 1;
string name = 2;
double price = 3;
}
message ListProductsResponse {
repeated Product products = 1;
}
-
Package:
catalog– used by both server and client when loading the proto. -
Service:
ProductService– two unary RPCs:GetProductandListProducts. - Messages: Request/response types with field numbers for Protocol Buffer encoding.
Both services load this file; the server implements it, the client generates stubs from it. See Protocol Buffers documentation for schema design best practices.
Product Service (gRPC Server)
Module Configuration
src/product-server/app.module.ts:
import { HazelModule } from '@hazeljs/core';
import { GrpcModule } from '@hazeljs/grpc';
import { join } from 'path';
import { ProductGrpcController } from './product.grpc-controller';
@HazelModule({
imports: [
GrpcModule.forRoot({
protoPath: join(__dirname, '../../proto/product.proto'),
package: 'catalog',
url: '0.0.0.0:50051',
}),
],
providers: [ProductGrpcController],
})
export class ProductAppModule {}
- GrpcModule.forRoot() – Configures the gRPC server with proto path, package name, and bind address.
-
providers – Registers
ProductGrpcControllerin the HazelJS DI container.
gRPC Controller
src/product-server/product.grpc-controller.ts:
import { Controller } from '@hazeljs/core';
import { GrpcMethod } from '@hazeljs/grpc';
const PRODUCTS: Array<{ id: string; name: string; price: number }> = [
{ id: '1', name: 'Widget', price: 9.99 },
{ id: '2', name: 'Gadget', price: 19.99 },
{ id: '3', name: 'Gizmo', price: 4.99 },
];
@Controller('')
export class ProductGrpcController {
@GrpcMethod('ProductService', 'GetProduct')
getProduct(data: { id: string }) {
const product = PRODUCTS.find((p) => p.id === data.id);
if (!product) throw new Error(`Product not found: ${data.id}`);
return product;
}
@GrpcMethod('ProductService', 'ListProducts')
listProducts(data: { limit?: number }) {
const limit = data.limit ?? 10;
return { products: PRODUCTS.slice(0, limit) };
}
}
- @GrpcMethod(serviceName, methodName) – Maps a class method to an RPC. See @hazeljs/grpc API.
- Handlers receive plain objects matching the proto request; return values match the response type.
Bootstrap
src/product-server/main.ts:
import { HazelApp } from '@hazeljs/core';
import { GrpcModule, GrpcServer } from '@hazeljs/grpc';
import { Container } from '@hazeljs/core';
import { ProductAppModule } from './app.module';
import { ProductGrpcController } from './product.grpc-controller';
async function bootstrap() {
const app = new HazelApp(ProductAppModule);
GrpcModule.registerHandlersFromProviders([ProductGrpcController]);
await app.listen(3001);
const grpcServer = Container.getInstance().resolve(GrpcServer);
await grpcServer.start();
console.log('Product gRPC server listening on 0.0.0.0:50051');
console.log('HTTP server (health) on http://localhost:3001');
}
bootstrap().catch((err) => {
console.error(err);
process.exit(1);
});
-
registerHandlersFromProviders() – Registers
@GrpcMethodhandlers from controller classes. - app.listen(3001) – Starts the HTTP server (e.g. for health checks).
- grpcServer.start() – Binds the gRPC server to the configured port.
Order Service (gRPC Client)
Module Configuration
src/order-client/app.module.ts:
import { HazelModule } from '@hazeljs/core';
import { GrpcClientModule } from '@hazeljs/grpc';
import { join } from 'path';
import { OrderService } from './order.service';
import { OrderController } from './order.controller';
@HazelModule({
imports: [
GrpcClientModule.forRoot({
protoPath: join(__dirname, '../../proto/product.proto'),
package: 'catalog',
defaultUrl: 'localhost:50051',
}),
],
providers: [OrderService],
controllers: [OrderController],
})
export class OrderAppModule {}
-
GrpcClientModule.forRoot() – Configures the gRPC client with the same proto and
defaultUrlfor the Product service. -
providers –
OrderService(usesGrpcClientService). -
controllers –
OrderController(HTTP routes).
Order Service (gRPC Client Usage)
src/order-client/order.service.ts:
import { Service } from '@hazeljs/core';
import { GrpcClientService } from '@hazeljs/grpc';
@Service()
export class OrderService {
constructor(private grpcClient: GrpcClientService) {}
async getProduct(productId: string) {
const client = this.grpcClient.getClient('ProductService');
return new Promise((resolve, reject) => {
client.GetProduct({ id: productId }, (err, res) => {
if (err) reject(err);
else resolve(res);
});
});
}
async listProducts(limit = 10) {
const client = this.grpcClient.getClient('ProductService');
return new Promise((resolve, reject) => {
client.ListProducts({ limit }, (err, res) => {
if (err) reject(err);
else resolve(res);
});
});
}
}
-
GrpcClientService – Injected by the container; provides
getClient(serviceName). -
getClient('ProductService') – Returns a stub with
GetProduct,ListProducts, etc. - Stub methods use the Node.js gRPC callback style; we wrap them in
Promisefor async/await.
Order Controller (HTTP API)
src/order-client/order.controller.ts:
import { Controller, Get, Param, Query } from '@hazeljs/core';
import { OrderService } from './order.service';
@Controller('')
export class OrderController {
constructor(private orderService: OrderService) {}
@Get('/products')
async listProducts(@Query('limit') limit?: string) {
const products = await this.orderService.listProducts(
limit ? parseInt(limit, 10) : 10
);
return products;
}
@Get('/products/:id')
async getProduct(@Param('id') id: string) {
return await this.orderService.getProduct(id);
}
}
- @Controller('') – Base path (empty = root).
-
@get() – HTTP GET routes;
@Queryand@Paramfor parameters. -
Constructor injection –
OrderServiceis injected by HazelJS core; no@Injectable()needed on controllers.
Running the Example
1. Install dependencies
cd hazeljs-grpc-starter
npm install
2. Start the Product gRPC server
npm run dev:product-server
This starts:
- gRPC server on
0.0.0.0:50051 - HTTP server on
http://localhost:3001(health checks)
3. Start the Order client service
In a second terminal:
npm run dev:order-client
This starts the Order service on http://localhost:3000.
4. Production build (optional)
npm run build
npm run start:product-server # Terminal 1
npm run start:order-client # Terminal 2
Testing the Flow
# List products (Order → gRPC → Product)
curl http://localhost:3000/products
# With limit
curl "http://localhost:3000/products?limit=2"
# Get product by ID
curl http://localhost:3000/products/1
Expected responses:
// GET /products
{"products":[{"id":"1","name":"Widget","price":9.99},{"id":"2","name":"Gadget","price":19.99},{"id":"3","name":"Gizmo","price":4.99}]}
// GET /products/1
{"id":"1","name":"Widget","price":9.99}
Key Concepts & Patterns
| Concept | Description |
|---|---|
| Proto as contract | Both server and client load the same .proto; the schema is the shared contract. |
| GrpcModule vs GrpcClientModule | Server uses GrpcModule; client uses GrpcClientModule. |
| @GrpcMethod | Decorator that maps a class method to an RPC. See @hazeljs/grpc README. |
| GrpcClientService.getClient() | Returns a cached stub for a service; use defaultUrl or pass url per call. |
| registerHandlersFromProviders() | Must be called before grpcServer.start() to wire @GrpcMethod handlers. |
| Dependency injection | Controllers and services are resolved from the HazelJS container; constructor injection works without @Injectable() on controllers. |
Further Reading & Resources
HazelJS
- HazelJS – Official site
- @hazeljs/core – Core framework (DI, routing, decorators)
- @hazeljs/grpc – gRPC module on npm
- HazelJS gRPC Documentation – Package docs
gRPC & Protocol Buffers
- gRPC – Official gRPC site
- gRPC Node.js Quick Start
- Protocol Buffers – Schema and encoding
- @grpc/grpc-js – Node.js gRPC library
- @grpc/proto-loader – Runtime proto loading
Related
- hazeljs-grpc-starter – This starter (if published)
- HazelJS GitHub – Monorepo
This blog post accompanies the HazelJS gRPC starter. For questions or contributions, see the HazelJS repository.
Top comments (0)