npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@rayondigital/nest-dapr

v0.9.73

Published

Develop NestJs microservices using Dapr pubsub, actors and other bindings

Downloads

221

Readme

npm version

NestJS + Dapr

Develop NestJs microservices using Dapr pubsub, actors and bindings.

Description

Dapr Module for Nest built on top of the Dapr JS SDK.

Supported features

Installation

npm i --save @rayondigital/nest-dapr

Requirements

Install Dapr as per getting started guide. Ensure Dapr is running with

dapr --version

Output:

CLI version: 1.12.0
Runtime version: 1.12.2

Quick start

The following scaffolds a Nest project with the nest-dapr package and demonstrates using Nest with Dapr using actors and RabbitMQ pubsub bindings.

Install Nest CLI

npm install -g @nestjs/cli

Scaffold Nest project

nest new nest-dapr
cd nest-dapr/

Install nest-dapr package

npm i --save @rayondigital/nest-dapr

Import DaprModule in AppModule class

@Module({
  imports: [DaprModule.register()],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Import DaprClient from @dapr/dapr package and add dependency to AppController class

import { DaprClient } from '@dapr/dapr';
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private readonly daprClient: DaprClient,
  ) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

Actors

Create actors and connect them to your NestJS application using the @DaprActor decorator. This decorator takes in the interface of the actor, and marks the Actor as transient inside the NestJS dependency injection container.

// You must expose your actors interface as an abstract class because Typescript interfaces are not available at runtime (erasure).
// Having the interface as an abstract class allows us to call the actor by only knowing the interface type.
export abstract class CounterActorInterface {
  abstract increment(): Promise<number>;
  abstract getCounter(): Promise<number>;
}

@DaprActor({
  interfaceType: CounterActorInterface,
})
export class CounterActor
  extends StatefulActor
  implements CounterActorInterface
{
  // You can inject other NestJS services into your actor.
  // Only Singleton services are supported at this time.
  @Inject(CacheService)
  private readonly cacheService: CacheService;

  counter: number;

  async onActivate(): Promise<void> {
    this.counter = await this.getState('counter', 0);
    return super.onActivate();
  }

  async increment(): Promise<number> {
    this.counter++;
    // Use a NestJS service as an example.
    // Share in memory state between actors on this node.
    // You probably will never want to do this, but we're just demonstrating a singleton service.
    await this.cacheService.increment('total');

    await this.setState('counter', this.counter);
    await this.saveState();
    return this.counter;
  }

  async getCounter(): Promise<number> {
    return this.counter;
  }
}

Actor Client

This module provides the DaprActorClient which is a NestJS service. It can be injected into controllers, services, handlers and other actors. It acts as a proxy service to the actors, and allows you to call methods on the actors - similar to the Orleans GrainFactory.

@Controller()
export class CounterController {
  constructor(
    private readonly actorClient: DaprActorClient,
  ) {}

  @Get(":id")
  async increment(@Param("id") id: string): Promise<string> {
    const value = await this.actorClient
      .getActor(CounterActorInterface, id)
      .increment();
    return `Counter incremented to ${value}`;
  }
}

PubSub

Create pubsub & topic names used for pubsub operations and message interface

const pubSubName = 'my-pubsub';
const topicName = 'my-topic';

interface Message {
  hello: string;
}

@Controller()

Create endpoint to publish topic message

@Post('pubsub')
async pubsub(): Promise<boolean> {
  const message: Message = { hello: 'world' };

  return this.daprClient.pubsub.publish(pubSubName, topicName, message);
}

Create pubsub handler which will subscribe to the topic and log the received message

@DaprPubSub(pubSubName, topicName)
pubSubHandler(message: Message): void {
  console.log(`Received topic:${topicName} message:`, message);
}

Create Dapr pubsub component in components folder

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: my-pubsub
  namespace: default
spec:
  type: pubsub.rabbitmq
  version: v1
  metadata:
  - name: host
    value: amqp://guest:guest@localhost:5674

Save file as components/rabbitmq-pubsub.yaml

Create docker-compose.yml in the project root used to run RabbitMQ

version: '3.9'
services:
  pubsub:
    image: rabbitmq:3-management-alpine
    ports:
      - 5674:5672
      - 15674:15672

Start RabbitMQ

docker-compose up

Create script to bootstrap your Nest project using Dapr sidecar. Update package.json and add script

"scripts": {
  ..
  "start:dapr": "dapr run --app-id nest-dapr --app-protocol http --app-port 50001 --dapr-http-port 50000 --components-path ./components npm run start"
},

Start Nest app with Dapr

npm run start:dapr

Invoke endpoint to publish the message

curl -X POST localhost:3000/pubsub

This should publish a message to RabbitMQ which should be consumed by the handler and written to the console:

== APP == Received topic:my-topic message: { hello: 'world' }

Full example

import { DaprClient } from '@dapr/dapr';
import { DaprPubSub } from '@rayondigital/nest-dapr';
import { Controller, Get, Post } from '@nestjs/common';
import { AppService } from './app.service';

const pubSubName = 'my-pubsub';
const topicName = 'my-topic';

interface Message {
  hello: string;
}

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private readonly daprClient: DaprClient,
  ) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Post('pubsub')
  async pubsub(): Promise<boolean> {
    const message: Message = { hello: 'world' };

    return this.daprClient.pubsub.publish(pubSubName, topicName, message);
  }

  @DaprPubSub(pubSubName, topicName)
  pubSubHandler(message: Message): void {
    console.log(`Received topic:${topicName} message:`, message);
  }
}

DaprModule

DaprModule is a global Nest Module used to register DaprServer & DaprClient as providers within your project. It also registers all your handlers which listen to Dapr pubsub and input bindings so that when messages are received by Dapr, they are forwarded to the handler. Handler registration occurs during the onApplicationBootstrap lifecycle hook.

To use nest-dapr, import the DaprModule into the root AppModule and run the register() static method.

@Module({
  imports: [DaprModule.register()],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

register() takes an optional DaprModuleOptions object which allows passing arguments to DaprServer instance.

export interface DaprModuleOptions {
  serverHost?: string;
  serverPort?: string;
  daprHost?: string;
  daprPort?: string;
  communicationProtocol?: CommunicationProtocolEnum;
  clientOptions?: DaprClientOptions;
}

See Dapr JS docs for more information about these arguments.

Async configuration

You can pass your options asynchronously instead of statically. In this case, use the registerAsync() method, which provides several ways to deal with async configuration. One of which is to use a factory function:

DaprModule.registerAsync({
  imports: [ConfigModule],
  useFactory: (configService: ConfigService) => ({
    serverHost: configService.get('DAPR_SERVER_HOST'),
    serverPort: configService.get('DAPR_SERVER_PORT'),
    daprHost: configService.get('DAPR_HOST'),
    daprPort: configService.get('DAPR_PORT'),
    communicationProtocol: CommunicationProtocolEnum.GRPC,
    clientOptions: {
      logger: {
        level: LogLevel.Verbose,
      },
    },
  }),
  inject: [ConfigService],
})

DaprServer & DaprClient providers

DaprModule registers DaprServer and DaprClient as Nest providers. These can be injected into your controllers and services like any other provider.

import { DaprClient } from '@dapr/dapr';
import { Controller, Post } from '@nestjs/common';

@Controller()
export class AppController {
  constructor(readonly daprClient: DaprClient) {}

  @Post()
  async pubsub(): Promise<boolean> {
    return this.daprClient.pubsub.publish('my-pub-sub', 'my-topic', {
      hello: 'world',
    });
  }
}

Dapr decorators

nest-dapr provides two TypeScript decorators which are used to declaratively configure subscriptions and bindings. These are used by DaprModule in conjunction with the handler method to define the handler implementations.

DaprPubSub decorator

DaprPubSub decorator is used to set-up a handler for receiving pubsub topic messages. The handler has 3 arguments (name, topicName & route). name specifies the pubsub component name as defined in the Dapr component metadata section. topicName is the name of the pubsub topic. Route is an optional argument and defines possible routing values.

Example:

@DaprPubSub('my-pubsub', 'my-topic')
pubSubHandler(message: any): void {
  console.log('Received message:', message);
}

RabbitMQ pubsub Component:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: my-pubsub
  namespace: default
spec:
  type: pubsub.rabbitmq
  version: v1
  metadata:
  - name: host
    value: amqp://guest:guest@localhost:5674

Publish message:

await this.daprClient.pubsub.publish('my-pubsub', 'my-topic', { hello: 'world' });

In this example the handler pubSubHandler method will receive messages from the my-topic topic through the my-pubsub component which in this case is RabbitMQ.

DaprBinding decorator

DaprBinding decorator is used to set-up a handler for receiving input binding data. The handler has one argument name which specifies the binding component name as defined in the Dapr component metadata section.

Example:

@DaprBinding('my-queue-binding')
bindingHandler(message: any): void {
  coneole.log('Received message:', message);
}

RabbitMQ binding component:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: my-queue-binding
  namespace: default
spec:
  type: bindings.rabbitmq
  version: v1
  metadata:
  - name: queueName
    value: queue1
  - name: host
    value: amqp://guest:guest@localhost:5674
  - name: durable
    value: true
  - name: deleteWhenUnused
    value: false
  - name: ttlInSeconds
    value: 60
  - name: prefetchCount
    value: 0
  - name: exclusive
    value: false
  - name: maxPriority
    value: 5
  - name: contentType
    value: "text/plain"

Send message:

await this.daprClient.binding.send('my-queue-binding', 'create', { hello: 'world' });

In this example the handler bindingHandler method will receive messages from the queue1 queue defined in the my-queue-binding component which in this case is RabbitMQ.

Writing handlers

DaprModule uses reflection to register all handlers found either in Controller or Provider classes. These classes must be registered in a Nest module. Providers must be decorated with the @Injectable() decorator at the class level. Once this is done and your provider is added to your module's [providers] array then nest-dapr will use Nest dependency injection container to resolve the provider instance and call your handler when the message is received.

Here's an example of a Provider containing a Dapr handler.

import { DaprPubSub } from '@rayondigital/nest-dapr';
import { Injectable, Logger } from '@nestjs/common';

@Injectable()
export class AppService {
  private readonly logger = new Logger(AppService.name);

  @DaprPubSub('my-pubsub', 'my-topic')
  pubSubHandler(message: any): void {
    this.logger.log(`Received topic message:`, message);
  }
}

Examples

| Example | Description | |---|-------------------------------------------------------------------------| | Basics | Demonstrates a very basic actors, pubsub & input binding using RabbitMQ |

Troubleshooting

Dapr is a complex set of tools and services and must be set-up and deployed carefully to ensure your system operates correctly. This library is merely integration using the existing Dapr js-sdk. If things are not working out for you please review:

  • Your configuration
  • Your Dapr local environment
  • Your port numbers and hostnames
  • Dapr & SDK documentation
  • The tests and examples in this project

If you find that both Dapr and the Javascript SDK is both working fine but nest-dapr is not working in some way, please file an issue and state clearly the problem and provide a reproducible code example. Filing an issue with something like: "It doesn't work" is likely to be ignored or removed.

Credits/Contributions :heart:

Thanks to:

Licence

Released under the MIT license. No warranty expressed or implied.