Getting started
@inversifyjs/grpc-js builds a @grpc/grpc-js server from classes registered in an Inversify container. Decorators, gRPC status errors, guards, middleware, interceptors, pipes, and error filters come from @inversifyjs/grpc-core.
Install dependencies
- npm
- pnpm
- yarn
npm install inversify reflect-metadata @grpc/grpc-js @bufbuild/protobuf @inversifyjs/grpc-core @inversifyjs/grpc-js
npm install -D grpc-tools ts-proto
pnpm add inversify reflect-metadata @grpc/grpc-js @bufbuild/protobuf @inversifyjs/grpc-core @inversifyjs/grpc-js
pnpm add -D grpc-tools ts-proto
yarn add inversify reflect-metadata @grpc/grpc-js @bufbuild/protobuf @inversifyjs/grpc-core @inversifyjs/grpc-js
yarn add -D grpc-tools ts-proto
Enable experimentalDecorators and emitDecoratorMetadata in tsconfig.json.
Import reflect-metadata once, before any decorated class is loaded.
Describe the service
syntax = "proto3";
package hero.v1;
service HeroService {
rpc GetHero (HeroRequest) returns (HeroResponse);
}
service HeroUploadService {
rpc UploadHeroes (stream UploadRequest) returns (UploadResponse);
}
service HeroListService {
rpc ListHeroes (HeroRequest) returns (stream HeroResponse);
}
service HeroChatService {
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
message HeroRequest {
string id = 1;
}
message HeroResponse {
string name = 1;
}
message UploadRequest {
string name = 1;
}
message UploadResponse {
repeated string names = 1;
}
message ChatMessage {
string text = 1;
}
Later pages use the upload, list, and chat services from this file.
Generate TypeScript
ts-proto writes a module from hero.proto. Import the message types and HeroServiceService from that module. HeroServiceService is the definition @Service() expects.
mkdir -p ./src/hero/generated
grpc_tools_node_protoc \
--plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto \
--ts_proto_out=./src/hero/generated \
--ts_proto_opt=esModuleInterop=true,importSuffix=.js,outputServices=grpc-js,env=node,useOptionals=none,outputJsonMethods=false,outputClientImpl=false \
--proto_path=./proto \
./proto/hero.proto
outputServices=grpc-js adds the service definition. @RPC() uses its keys, so rpc GetHero is getHero.
Implement the service
import { type Server, ServerCredentials } from '@grpc/grpc-js';
import { RPC, Service } from '@inversifyjs/grpc-core';
import { InversifyGrpcJsAdapter } from '@inversifyjs/grpc-js';
import { Container } from 'inversify';
import {
type HeroRequest,
type HeroResponse,
HeroServiceService,
} from './generated/hero.js';
@Service(HeroServiceService)
export class HeroService {
@RPC('getHero')
public getHero(call: { request: HeroRequest }): HeroResponse {
return {
name: call.request.id,
};
}
}
export interface StartedGrpcServer {
port: number;
server: Server;
}
export async function startGrpcServer(
address: string,
): Promise<StartedGrpcServer> {
const container: Container = new Container();
container.bind(HeroService).toSelf();
const adapter: InversifyGrpcJsAdapter = new InversifyGrpcJsAdapter(
container,
{
logger: false,
},
);
const server: Server = await adapter.build();
const port: number = await new Promise<number>(
(
resolve: (boundPort: number) => void,
reject: (error: Error) => void,
): void => {
server.bindAsync(
address,
ServerCredentials.createInsecure(),
(error: Error | null, boundPort: number): void => {
if (error !== null) {
reject(error);
return;
}
resolve(boundPort);
},
);
},
);
return {
port,
server,
};
}
export async function main(): Promise<void> {
const started: StartedGrpcServer = await startGrpcServer('0.0.0.0:50051');
console.log(`gRPC server is listening on port ${started.port.toString()}`);
}
Start the server
HeroService handles getHero. Call main() to listen on 0.0.0.0:50051, or call startGrpcServer() with another address. build() returns the @grpc/grpc-js Server. Binding and credentials stay in your code, so this example can use an ephemeral port in tests and ServerCredentials.createSsl in production.
logger: false turns off the startup log. Leave logger unset to print each service and RPC when the server is built. Pass a Logger to use your own.
Bind every service class you want to expose before build(). @Service() makes the class injectable. The adapter registers a service only when its service identifier is bound. Import the module that declares the class so the decorator runs.
Request lifecycle
For each RPC, the adapter runs:
- Pre-handler middleware, global first, then class, then method.
- Guards, in that same order.
- Interceptors. Class interceptors wrap method interceptors, which wrap global interceptors.
- Pipes on decorated parameters. Global pipes run before parameter pipes.
- The RPC method.
- Post-handler middleware: class, then method, then global.
A guard that returns false ends the call with PERMISSION_DENIED. A thrown GrpcError becomes the gRPC status. Anything else becomes status UNKNOWN with the details Unknown, and the original error is logged when logging is enabled.
Continue with Services.