Skip to main content

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 install inversify reflect-metadata @grpc/grpc-js @bufbuild/protobuf @inversifyjs/grpc-core @inversifyjs/grpc-js
npm install -D grpc-tools ts-proto
warning

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:

  1. Pre-handler middleware, global first, then class, then method.
  2. Guards, in that same order.
  3. Interceptors. Class interceptors wrap method interceptors, which wrap global interceptors.
  4. Pipes on decorated parameters. Global pipes run before parameter pipes.
  5. The RPC method.
  6. 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.