Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
The nice-grpc package is a modern, easy-to-use gRPC library for Node.js. It provides a more ergonomic API compared to the official gRPC library, with features like middleware support, automatic retries, and better TypeScript support.
Creating a gRPC Server
This code demonstrates how to create a gRPC server using nice-grpc. The server listens on port 50051 and implements a simple Greeter service that responds with a greeting message.
const { createServer } = require('nice-grpc');
const { GreeterService } = require('./proto/greeter_grpc_pb');
const server = createServer();
server.add(GreeterService, {
async sayHello(request) {
return { message: `Hello, ${request.name}!` };
},
});
server.listen('0.0.0.0:50051');
Creating a gRPC Client
This code demonstrates how to create a gRPC client using nice-grpc. The client connects to a server running on localhost:50051 and calls the sayHello method of the Greeter service.
const { createChannel, createClient } = require('nice-grpc');
const { GreeterClient } = require('./proto/greeter_grpc_pb');
const channel = createChannel('localhost:50051');
const client = createClient(GreeterClient, channel);
async function main() {
const response = await client.sayHello({ name: 'World' });
console.log(response.message);
}
main();
Using Middleware
This code demonstrates how to use middleware in nice-grpc. The loggingMiddleware logs the request and response of each gRPC call. The middleware is added to the server using the use method.
const { createServer, createChannel, createClient, Metadata } = require('nice-grpc');
const { GreeterService, GreeterClient } = require('./proto/greeter_grpc_pb');
const loggingMiddleware = async (call, context, next) => {
console.log('Request:', call.request);
const response = await next(call, context);
console.log('Response:', response);
return response;
};
const server = createServer().use(loggingMiddleware);
server.add(GreeterService, {
async sayHello(request) {
return { message: `Hello, ${request.name}!` };
},
});
server.listen('0.0.0.0:50051');
The official gRPC library for Node.js. It provides a comprehensive set of features for building gRPC clients and servers but has a more complex API compared to nice-grpc. It lacks some of the ergonomic improvements and middleware support found in nice-grpc.
Another pure JavaScript implementation of gRPC for Node.js, maintained by the gRPC team. It aims to be a drop-in replacement for the grpc package with better support for modern JavaScript and TypeScript. Like grpc-js, it has a more complex API compared to nice-grpc.
A Node.js gRPC library that is nice to you. Built on top of
grpc-js
.
AbortSignal
.npm install nice-grpc
The recommended way is to use
ts-proto
.
ts-proto
Install necessary tools:
npm install protobufjs long
npm install --save-dev grpc-tools ts-proto
Given a Protobuf file ./proto/example.proto
, generate TypeScript code into
directory ./compiled_proto
:
./node_modules/.bin/grpc_tools_node_protoc \
--plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto \
--ts_proto_out=./compiled_proto \
--ts_proto_opt=outputServices=generic-definitions,useExactTypes=false \
--proto_path=./proto \
./proto/example.proto
You can omit the
--plugin
flag if you invoke this command via npm script.
google-protobuf
Install necessary tools:
npm install google-protobuf
npm install --save-dev grpc-tools grpc_tools_node_protoc_ts @types/google-protobuf
Given a Protobuf file ./proto/example.proto
, generate JS code and TypeScript
definitions into directory ./compiled_proto
:
./node_modules/.bin/grpc_tools_node_protoc \
--plugin=protoc-gen-grpc=./node_modules/.bin/grpc_tools_node_protoc_plugin \
--plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \
--js_out=import_style=commonjs,binary:./compiled_proto \
--ts_out=grpc_js:./compiled_proto \
--grpc_out=grpc_js:./compiled_proto \
--proto_path=./proto \
./proto/example.proto
Consider the following Protobuf definition:
syntax = "proto3";
package nice_grpc.example;
service ExampleService {
rpc ExampleUnaryMethod(ExampleRequest) returns (ExampleResponse) {};
}
message ExampleRequest {
// ...
}
message ExampleResponse {
// ...
}
After compiling Protobuf file, we can write service implementation:
When compiling Protobufs using ts-proto
:
import {ServiceImplementation} from 'nice-grpc';
import {
ExampleServiceDefinition,
ExampleRequest,
ExampleResponse,
DeepPartial,
} from './compiled_proto/example';
const exampleServiceImpl: ServiceImplementation<
typeof ExampleServiceDefinition
> = {
async exampleUnaryMethod(
request: ExampleRequest,
): Promise<DeepPartial<ExampleResponse>> {
// ... method logic
return response;
},
};
Alternatively, you can use classes:
class ExampleServiceImpl
implements ServiceImplementation<typeof ExampleServiceDefinition>
{
async exampleUnaryMethod(
request: ExampleRequest,
): Promise<DeepPartial<ExampleResponse>> {
// ... method logic
return response;
}
}
With ts-proto
, response is automatically wrapped with fromPartial
.
When compiling Protobufs using google-protobuf
:
import {ServiceImplementation} from 'nice-grpc';
import {ExampleRequest, ExampleResponse} from './compiled_proto/example_pb';
import {IExampleService} from './compiled_proto/example_grpc_pb';
const exampleServiceImpl: ServiceImplementation<IExampleService> = {
async exampleUnaryMethod(request: ExampleRequest): Promise<ExampleResponse> {
// ... method logic
return response;
},
};
Further examples use ts-proto
.
Now we can create and start a server that exposes our service:
import {createServer} from 'nice-grpc';
import {ExampleServiceDefinition} from './compiled_proto/example';
const server = createServer();
server.add(ExampleServiceDefinition, exampleServiceImpl);
await server.listen('0.0.0.0:8080');
Once we need to stop, gracefully shut down the server:
await server.shutdown();
Each service implementation method receives CallContext
as a second argument,
that has type:
type CallContext = {
/**
* Request metadata from client.
*/
metadata: Metadata;
/**
* Client address.
*/
peer: string;
/**
* Response header. Sent with the first response, or when `sendHeader` is
* called.
*/
header: Metadata;
/**
* Manually send response header.
*/
sendHeader(): void;
/**
* Response trailer. Sent when server method returns or throws.
*/
trailer: Metadata;
/**
* Signal that is aborted once the call gets cancelled.
*/
signal: AbortSignal;
};
Call context may be augmented by Middleware.
To report an error to a client, use ServerError
.
Any thrown errors other than
ServerError
will result in client receiving error with status codeUNKNOWN
. Use server middleware for custom handling of uncaught errors.
See gRPC docs for the correct usage of status codes.
import {ServerError, Status} from 'nice-grpc';
const exampleServiceImpl: ServiceImplementation<
typeof ExampleServiceDefinition
> = {
async exampleUnaryMethod(
request: ExampleRequest,
): Promise<DeepPartial<ExampleResponse>> {
// ... method logic
throw new ServerError(Status.NOT_FOUND, 'Requested data does not exist');
},
};
A server receives client metadata along with request, and can send response metadata in header and trailer.
const exampleServiceImpl: ServiceImplementation<
typeof ExampleServiceDefinition
> = {
async exampleUnaryMethod(
request: ExampleRequest,
context: CallContext,
): Promise<DeepPartial<ExampleResponse>> {
// read client metadata
const someValue = context.metadata.get('some-key');
// add metadata to header
context.header.set('some-key', 'some-value');
// ... method logic
// add metadata to trailer
context.trailer.set('some-key', 'some-value');
return response;
},
};
A server receives
AbortSignal
that gets aborted once the call is cancelled by the client. You can use it to
cancel any inner requests.
import fetch from 'node-fetch';
const exampleServiceImpl: ServiceImplementation<
typeof ExampleServiceDefinition
> = {
async exampleUnaryMethod(
request: ExampleRequest,
context: CallContext,
): Promise<DeepPartial<ExampleResponse>> {
const response = await fetch('http://example.com', {
signal: context.signal,
});
// ...
},
};
Consider the following Protobuf definition:
service ExampleService {
rpc ExampleStreamingMethod(ExampleRequest)
returns (stream ExampleResponse) {};
}
Service implementation defines this method as an Async Generator:
import {delay} from 'abort-controller-x';
const exampleServiceImpl: ServiceImplementation<
typeof ExampleServiceDefinition
> = {
async *exampleStreamingMethod(
request: ExampleRequest,
context: CallContext,
): AsyncIterable<DeepPartial<ExampleResponse>> {
for (let i = 0; i < 10; i++) {
await delay(context.signal, 1000);
yield response;
}
},
};
import {range} from 'ix/asynciterable';
import {withAbort, map} from 'ix/asynciterable/operators';
const exampleServiceImpl: ServiceImplementation<
typeof ExampleServiceDefinition
> = {
async *exampleStreamingMethod(
request: ExampleRequest,
context: CallContext,
): AsyncIterable<DeepPartial<ExampleResponse>> {
yield* range(0, 10).pipe(
withAbort(context.signal),
map(() => response),
);
},
};
import {Observable} from 'rxjs';
import {from} from 'ix/asynciterable';
import {withAbort} from 'ix/asynciterable/operators';
const exampleServiceImpl: ServiceImplementation<
typeof ExampleServiceDefinition
> = {
async *exampleStreamingMethod(
request: ExampleRequest,
context: CallContext,
): AsyncIterable<DeepPartial<ExampleResponse>> {
const observable: Observable<DeepPartial<ExampleResponse>>;
yield* from(observable).pipe(withAbort(context.signal));
},
};
Given a client streaming method:
service ExampleService {
rpc ExampleClientStreamingMethod(stream ExampleRequest)
returns (ExampleResponse) {};
}
Service implementation method receives request as an Async Iterable:
const exampleServiceImpl: ServiceImplementation<
typeof ExampleServiceDefinition
> = {
async exampleUnaryMethod(
request: AsyncIterable<ExampleRequest>,
): Promise<DeepPartial<ExampleResponse>> {
for await (const item of request) {
// ...
}
return response;
},
};
Server middleware intercepts incoming calls allowing to:
ServerError
ServerError
s to a clientServer middleware is defined as an Async Generator. The most basic no-op middleware looks like this:
import {ServerMiddlewareCall, CallContext} from 'nice-grpc';
async function* middleware<Request, Response>(
call: ServerMiddlewareCall<Request, Response>,
context: CallContext,
) {
return yield* call.next(call.request, context);
}
For unary and client streaming methods, the call.next
generator yields no
items and returns a single response; for server streaming and bidirectional
streaming methods, it yields each response and returns void. By doing
return yield*
we cover both cases. To handle these cases separately, we can
write a middleware as follows:
async function* middleware<Request, Response>(
call: ServerMiddlewareCall<Request, Response>,
context: CallContext,
) {
if (!call.responseStream) {
const response = yield* call.next(call.request, context);
return response;
} else {
for await (const response of call.next(call.request, context)) {
yield response;
}
return;
}
}
To attach a middleware to a server, use a server.use
method. Note that
server.use
returns a new server instance.
const server = createServer().use(middleware1).use(middleware2);
A middleware that is attached first, will be invoked first.
You can also attach middleware per-service:
const server = createServer().use(middlewareA);
server.with(middlewareB).add(Service1, service1Impl);
server.with(middlewareC).add(Service2, service2Impl);
In the above example, Service1
gets middlewareA
and middlewareB
, and
Service2
gets middlewareA
and middlewareC
.
Log all calls:
import {Status} from 'nice-grpc';
import {isAbortError} from 'abort-controller-x';
async function* loggingMiddleware<Request, Response>(
call: ServerMiddlewareCall<Request, Response>,
context: CallContext,
) {
const {path} = call.method;
console.log('Server call', path, 'start');
try {
const result = yield* call.next(call.request, context);
console.log('Server call', path, 'end: OK');
return result;
} catch (error) {
if (error instanceof ServerError) {
console.log(
'Server call',
path,
`end: ${Status[error.code]}: ${error.details}`,
);
} else if (isAbortError(error)) {
console.log('Server call', path, 'cancel');
} else {
console.log('Server call', path, `error: ${error?.stack}`);
}
throw error;
}
}
Catch unknown errors and wrap them into ServerError
s with friendly messages:
import {Status} from 'nice-grpc';
import {isAbortError} from 'abort-controller-x';
async function* errorHandlingMiddleware<Request, Response>(
call: ServerMiddlewareCall<Request, Response>,
context: CallContext,
) {
try {
return yield* call.next(call.request, context);
} catch (error: unknown) {
if (error instanceof ServerError || isAbortError(error)) {
throw error;
}
let details = 'Unknown server error occurred';
if (process.env.NODE_ENV === 'development') {
details += `: ${error.stack}`;
}
throw new ServerError(Status.UNKNOWN, details);
}
}
Validate JSON Web Token (JWT) from request metadata and put its claims to
CallContext
:
import {Status} from 'nice-grpc';
import createRemoteJWKSet from 'jose/jwks/remote';
import jwtVerify, {JWTPayload} from 'jose/jwt/verify';
import {JOSEError} from 'jose/util/errors';
const jwks = createRemoteJWKSet(
new URL('https://example.com/.well-known/jwks.json'),
);
type AuthCallContextExt = {
auth: JWTPayload;
};
async function* authMiddleware<Request, Response>(
call: ServerMiddlewareCall<Request, Response, AuthCallContextExt>,
context: CallContext,
) {
const authorization = context.metadata.get('Authorization');
if (authorization == null) {
throw new ServerError(
Status.UNAUTHENTICATED,
'Missing Authorization metadata',
);
}
const parts = authorization.toString().split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
throw new ServerError(
Status.UNAUTHENTICATED,
'Invalid Authorization metadata format. Expected "Bearer <token>"',
);
}
const token = parts[1];
const {payload} = await jwtVerify(token, jwks).catch(error => {
if (error instanceof JOSEError) {
throw new ServerError(Status.UNAUTHENTICATED, error.message);
} else {
throw error;
}
});
return yield* call.next(call.request, {
...context,
auth: payload,
});
}
Service implementation can then access JWT claims via call context:
const exampleServiceImpl: ServiceImplementation<
typeof ExampleServiceDefinition,
AuthCallContextExt
> = {
async exampleUnaryMethod(
request: ExampleRequest,
context: CallContext & AuthCallContextExt,
): Promise<DeepPartial<ExampleResponse>> {
const userId = context.auth.sub;
// ...
},
};
Consider the following Protobuf definition:
syntax = "proto3";
package nice_grpc.example;
service ExampleService {
rpc ExampleUnaryMethod(ExampleRequest) returns (ExampleResponse) {};
}
message ExampleRequest {
// ...
}
message ExampleResponse {
// ...
}
After compiling Protobuf file, we can create the client:
When compiling Protobufs using ts-proto
:
import {createChannel, createClient, Client} from 'nice-grpc';
import {ExampleServiceDefinition} from './compiled_proto/example';
const channel = createChannel('localhost:8080');
const client: Client<typeof ExampleServiceDefinition> = createClient(
ExampleServiceDefinition,
channel,
);
When compiling Protobufs using google-protobuf
:
import {createChannel, createClient, Client} from 'nice-grpc';
import {
ExampleService,
IExampleService,
} from './compiled_proto/example_grpc_pb';
const channel = createChannel('localhost:8080');
const client: Client<IExampleService> = createClient(ExampleService, channel);
Further examples use ts-proto
.
Call the method:
const response = await client.exampleUnaryMethod(request);
With ts-proto
, request is automatically wrapped with fromPartial
.
Once we've done with the client, close the channel:
channel.close();
Each client method accepts CallOptions
as an optional second argument, that
has type:
type CallOptions = {
/**
* Request metadata.
*/
metadata?: Metadata;
/**
* Signal that cancels the call once aborted.
*/
signal?: AbortSignal;
/**
* Called when header is received.
*/
onHeader?(header: Metadata): void;
/**
* Called when trailer is received.
*/
onTrailer?(trailer: Metadata): void;
};
Call options may be augmented by Middleware.
When creating a client, you may specify default call options per method, or for all methods. This doesn't make much sense for built-in options, but may do for middleware.
const client = createClient(ExampleServiceDefinition, channel, {
'*': {
// applies for all methods
},
exampleUnaryMethod: {
// applies for single method
},
});
By default, a channel uses insecure connection. The following are equivalent:
import {createChannel, ChannelCredentials} from 'nice-grpc';
createChannel('example.com:8080');
createChannel('http://example.com:8080');
createChannel('example.com:8080', ChannelCredentials.createInsecure());
To connect over TLS, use one of the following:
createChannel('https://example.com:8080');
createChannel('example.com:8080', ChannelCredentials.createSsl());
If the port is omitted, it defaults to 80
for insecure connections, and 443
for secure connections.
Client can send request metadata and receive response header and trailer:
import {Metadata} from 'nice-grpc';
const response = await client.exampleUnaryMethod(request, {
metadata: Metadata({key: 'value'}),
onHeader(header: Metadata) {
// ...
},
onTrailer(trailer: Metadata) {
// ...
},
});
Client calls may throw gRPC errors represented as ClientError
, that contain
status code and description.
import {ClientError, Status} from 'nice-grpc';
import {ExampleResponse} from './compiled_proto/example';
let response: ExampleResponse | null;
try {
response = await client.exampleUnaryMethod(request);
} catch (error: unknown) {
if (error instanceof ClientError && error.code === Status.NOT_FOUND) {
response = null;
} else {
throw error;
}
}
A client call can be cancelled using
AbortSignal
.
import AbortController from 'node-abort-controller';
import {isAbortError} from 'abort-controller-x';
const abortController = new AbortController();
client
.exampleUnaryMethod(request, {
signal: abortController.signal,
})
.catch(error => {
if (isAbortError(error)) {
// aborted
} else {
throw error;
}
});
abortController.abort();
Consider the following Protobuf definition:
service ExampleService {
rpc ExampleStreamingMethod(ExampleRequest)
returns (stream ExampleResponse) {};
}
Client method returns an Async Iterable:
for await (const response of client.exampleStreamingMethod(request)) {
// ...
}
Given a client streaming method:
service ExampleService {
rpc ExampleClientStreamingMethod(stream ExampleRequest)
returns (ExampleResponse) {};
}
Client method expects an Async Iterable as its first argument:
import {ExampleRequest, DeepPartial} from './compiled_proto/example';
async function* createRequest(): AsyncIterable<DeepPartial<ExampleRequest>> {
for (let i = 0; i < 10; i++) {
yield request;
}
}
const response = await client.exampleClientStreamingMethod(createRequest());
Client middleware intercepts outgoing calls allowing to:
Client middleware is defined as an Async Generator and is very similar to Server middleware. Key differences:
CallContext
for client middleware; instead,
CallOptions
are passed through the chain and can be accessed or altered by a
middleware.The most basic no-op middleware looks like this:
import {ClientMiddlewareCall, CallOptions} from 'nice-grpc';
async function* middleware<Request, Response>(
call: ClientMiddlewareCall<Request, Response>,
options: CallOptions,
) {
return yield* call.next(call.request, options);
}
For unary and client streaming methods, the call.next
generator yields no
items and returns a single response; for server streaming and bidirectional
streaming methods, it yields each response and returns void. By doing
return yield*
we cover both cases. To handle these cases separately, we can
write a middleware as follows:
async function* middleware<Request, Response>(
call: ClientMiddlewareCall<Request, Response>,
options: CallOptions,
) {
if (!call.responseStream) {
const response = yield* call.next(call.request, options);
return response;
} else {
for await (const response of call.next(call.request, options)) {
yield response;
}
return;
}
}
To create a client with middleware, use a client factory:
import {createClientFactory} from 'nice-grpc';
const client = createClientFactory()
.use(middleware1)
.use(middleware2)
.create(ExampleService, channel);
A middleware that is attached first, will be invoked last.
You can reuse a single factory to create multiple clients:
const clientFactory = createClientFactory().use(middleware);
const client1 = clientFactory.create(Service1, channel1);
const client2 = clientFactory.create(Service2, channel2);
You can also attach middleware per-client:
const factory = createClientFactory().use(middlewareA);
const client1 = clientFactory.use(middlewareB).create(Service1, channel1);
const client2 = clientFactory.use(middlewareC).create(Service2, channel2);
In the above example, Service1
client gets middlewareA
and middlewareB
,
and Service2
client gets middlewareA
and middlewareC
.
Log all calls:
import {
ClientMiddlewareCall,
CallOptions,
ClientError,
Status,
} from 'nice-grpc';
import {isAbortError} from 'abort-controller-x';
async function* loggingMiddleware<Request, Response>(
call: ClientMiddlewareCall<Request, Response>,
options: CallOptions,
) {
const {path} = call.method;
console.log('Client call', path, 'start');
try {
const result = yield* call.next(call.request, options);
console.log('Client call', path, 'end: OK');
return result;
} catch (error) {
if (error instanceof ClientError) {
console.log(
'Client call',
path,
`end: ${Status[error.code]}: ${error.details}`,
);
} else if (isAbortError(error)) {
console.log('Client call', path, 'cancel');
} else {
console.log('Client call', path, `error: ${error?.stack}`);
}
throw error;
}
}
FAQs
A Node.js gRPC library that is nice to you
The npm package nice-grpc receives a total of 109,769 weekly downloads. As such, nice-grpc popularity was classified as popular.
We found that nice-grpc demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.