Skip to content

A dataloader implementation to talk with gRPC servers, with support for request merging and cross-request caching.

License

Notifications You must be signed in to change notification settings

moveaxlab/nestjs-grpc-client

Repository files navigation

NestJS gRPC Client

NPM npm Static Badge

This library defines gRPC dataloaders you can inject dynamically in GraphQL resolvers in order to optimize calls via batching and caching.

More about the dataloader pattern at https://github.com/graphql/dataloader

Installation

yarn add @moveaxlab/nestjs-grpc-client

Usage

Dataloaders

gRPC dataloaders are based on gRPC clients defined as follows:

// package.client.ts
import { GrpcClientForService } from '@moveax/nestjs-grpc-client';
import { grpcPkg } from 'somewhere'; // import also your autogenerated gRPC package

export type PackageClient = GrpcClientForService<
    grpcPkg.PackageServiceDefinition
>;
// OR
export type PackageClientWithStreamingMethods = GrpcClientForService<
    grpcPkg.PackageServiceDefinition,
    'myClientStreamingMethod' | 'myBidirectionalStreamingMethod'
>;

Implement your gRPC dataloader:

// package.dataloader.ts
import { Metadata } from '@grpc/grpc-js';
import { GrpcDataLoaderProvider, DataLoaderForClient, Request, createDataLoaderDecorator } from '@moveax/nestjs-grpc-client';
import { Injectable, Logger } from '@nestjs/common';
import { PackageClient } from 'package.client';
import { grpcPkg } from 'somewhere'; // import also your autogenerated gRPC package

@Injectable()
export class PackageDataLoaderProvider extends GrpcDataLoaderProvider<PackageClient> {
    client: PackageClient;
    logger = new Logger(PackageDataLoaderProvider.name);

    createMetadata(_: Request): Metadata {
        const metadata = new Metadata();
        // you can use the Express' request token to authenticate also the gRPC call 
        metadata.set('authorization', req.get('authorization'));
        return metadata;
    }

   get cacheConfig() {
      return {
         someMethod: {
            cacheKeyFn: (request: grpcPkg.ISomeMethodRequest) => {
               return `pkgService.someMethod:${input.param1}-${input.param2}`;
            },
            ttl: 60,
         },
      };
   }
}

export const PackageService = createDataLoaderDecorator(PackageDataLoaderProvider.prototype);

export type PackageDataloader = DataLoaderForClient<PackageClient>;
  1. The cacheConfig getter returns an object containing cache configuration for the various methods. If a method needs to be cached, the cacheConfig must contain an entry for that method that takes in input the method request and returns a string, which will be used as the cache key. A TTL can be specified, that will be used if global caching is enabled (see below).
  2. The PackageService is an object containing a parameter decorator for each method of the gRPC client. You can use the decorator in your resolvers to obtain an instance of the dataloader (see below).
  3. The PackageDataloader is a type containing the types of each method dataloader. You can use the type in your resolvers to add type safety to your loaders.

Add the GrpcDataLoaderInterceptor to your app in order to load dataloaders relevant for the request at runtime:

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { GrpcDataLoaderInterceptor } from '@moveax/nestjs-grpc-client';

@Module({
  provides: [
    {
      provide: APP_INTERCEPTOR,
      useClass: GrpcDataLoaderInterceptor,
    }
  ],
  /* ... */
})
export class AppModule {}

Now you can use the dataloader in your resolvers:

// imports omitted for lack of will

@Resolver()
class Whatever {
    @Query()
    async getWhatever(
        @PackageService.someMethod() loader: PackageDataloader['someMethod']
    ) {
        const response = await loader.load(/* gRPC request */);
        // do whatever you want with the response now
    }
}

Batching requests

To batch several requests to a given service when doing API composition, specify the mergeConfig inside your dataloader provider.

The mergeConfig provides two properties for each endpoint:

  • mergeInput, that takes an array of requests and combines them to a single request
  • splitOutput, that takes the original array of requests and a single response, and splits it into an array of responses

Global caching

To use global caching, pass true to the loader decorator:

// imports omitted for lack of will

@Resolver()
class Whatever {
    @Query()
    async getWhatever(
        @PackageService.someMethod(true) loader: PackageDataloader['someMethod']
    ) {
        const response = await loader.load(/* gRPC request */);
        // do whatever you want with the response now
    }
}

The ttl must be set to something greater than 0 inside the cacheConfig for the dataloader, for global caching to work. The TTL is expressed in seconds.

All requests to that specific dataloader will be cached for ttl seconds, in a cache shared between all calls. Use global caching with care: the global cache will skip all authorization logic that may live inside your backend services.

About

A dataloader implementation to talk with gRPC servers, with support for request merging and cross-request caching.

Topics

Resources

License

Stars

Watchers

Forks